1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-14 10:26:19 +01:00

Merge branch 'main' into auth/pm-4816/create-shared-LoginApprovalComponent

This commit is contained in:
Alec Rippberger 2024-11-07 15:33:26 -06:00
commit 3cb7ce4597
No known key found for this signature in database
GPG Key ID: 9DD8DA583B28154A
290 changed files with 9698 additions and 6610 deletions

20
.github/renovate.json vendored
View File

@ -73,7 +73,7 @@
"reviewers": ["team:team-admin-console-dev"]
},
{
"matchPackageNames": ["@types/node-ipc", "node-ipc", "qrious", "regedit"],
"matchPackageNames": ["@types/node-ipc", "node-ipc", "qrious"],
"description": "Auth owned dependencies",
"commitMessagePrefix": "[deps] Auth:",
"reviewers": ["team:team-auth-dev"]
@ -110,6 +110,8 @@
},
{
"matchPackageNames": [
"@electron/notarize",
"@electron/rebuild",
"@types/argon2-browser",
"@types/chrome",
"@types/firefox-webext-browser",
@ -119,6 +121,12 @@
"argon2",
"argon2-browser",
"big-integer",
"electron-builder",
"electron-log",
"electron-reload",
"electron-store",
"electron-updater",
"electron",
"node-forge",
"rxjs",
"type-fest",
@ -197,19 +205,11 @@
},
{
"matchPackageNames": [
"@electron/notarize",
"@electron/rebuild",
"@microsoft/signalr-protocol-msgpack",
"@microsoft/signalr",
"@types/jsdom",
"@types/papaparse",
"@types/zxcvbn",
"electron-builder",
"electron-log",
"electron-reload",
"electron-store",
"electron-updater",
"electron",
"jsdom",
"jszip",
"oidc-client-ts",
@ -258,5 +258,5 @@
"reviewers": ["team:team-vault-dev"]
}
],
"ignoreDeps": ["@types/koa-bodyparser", "bootstrap", "node-ipc", "node", "npm", "regedit"]
"ignoreDeps": ["@types/koa-bodyparser", "bootstrap", "node-ipc", "node", "npm"]
}

View File

@ -1,7 +1,8 @@
name: Build Browser
on:
pull_request:
pull_request_target:
types: [opened, synchronize]
branches-ignore:
- 'l10n_master'
- 'cf-pages'
@ -33,6 +34,10 @@ defaults:
shell: bash
jobs:
check-run:
name: Check PR run
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
setup:
name: Setup
runs-on: ubuntu-22.04
@ -41,8 +46,10 @@ jobs:
adj_build_number: ${{ steps.gen_vars.outputs.adj_build_number }}
node_version: ${{ steps.retrieve-node-version.outputs.node_version }}
steps:
- name: Checkout repo
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Get Package Version
id: gen_vars
@ -71,8 +78,10 @@ jobs:
run:
working-directory: apps/browser
steps:
- name: Checkout repo
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Testing locales - extName length
run: |
@ -109,8 +118,10 @@ jobs:
_BUILD_NUMBER: ${{ needs.setup.outputs.adj_build_number }}
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
steps:
- name: Checkout repo
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
@ -163,10 +174,6 @@ jobs:
run: npm run dist:mv3
working-directory: browser-source/apps/browser
- name: Build Chrome Manifest v3 Beta
run: npm run dist:chrome:beta
working-directory: browser-source/apps/browser
- name: Upload Opera artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
@ -188,13 +195,6 @@ jobs:
path: browser-source/apps/browser/dist/dist-chrome-mv3.zip
if-no-files-found: error
- name: Upload Chrome MV3 Beta artifact (DO NOT USE FOR PROD)
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: DO-NOT-USE-FOR-PROD-dist-chrome-MV3-beta-${{ env._BUILD_NUMBER }}.zip
path: browser-source/apps/browser/dist/dist-chrome-mv3-beta.zip
if-no-files-found: error
- name: Upload Firefox artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
@ -236,12 +236,15 @@ jobs:
needs:
- setup
- locales-test
- check-run
env:
_BUILD_NUMBER: ${{ needs.setup.outputs.adj_build_number }}
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
steps:
- name: Checkout repo
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
@ -353,8 +356,10 @@ jobs:
- build
- build-safari
steps:
- name: Checkout repo
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Login to Azure
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
@ -392,7 +397,10 @@ jobs:
- crowdin-push
steps:
- name: Check if any job failed
if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') && contains(needs.*.result, 'failure')
if: |
github.event_name != 'pull_request_target'
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
&& contains(needs.*.result, 'failure')
run: exit 1
- name: Login to Azure - Prod Subscription

View File

@ -1,7 +1,8 @@
name: Build CLI
on:
pull_request:
pull_request_target:
types: [opened, synchronize]
branches-ignore:
- 'l10n_master'
- 'cf-pages'
@ -34,6 +35,10 @@ defaults:
working-directory: apps/cli
jobs:
check-run:
name: Check PR run
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
setup:
name: Setup
runs-on: ubuntu-22.04
@ -41,8 +46,10 @@ jobs:
package_version: ${{ steps.retrieve-package-version.outputs.package_version }}
node_version: ${{ steps.retrieve-node-version.outputs.node_version }}
steps:
- name: Checkout repo
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Get Package Version
id: retrieve-package-version
@ -58,7 +65,6 @@ jobs:
NODE_VERSION=${NODE_NVMRC/v/''}
echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT
cli:
name: "${{ matrix.os.base }} - ${{ matrix.license_type.readable }}"
strategy:
@ -82,8 +88,10 @@ jobs:
_WIN_PKG_FETCH_VERSION: 20.11.1
_WIN_PKG_VERSION: 3.5
steps:
- name: Checkout repo
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Setup Unix Vars
run: |
@ -160,8 +168,10 @@ jobs:
_WIN_PKG_FETCH_VERSION: 20.11.1
_WIN_PKG_VERSION: 3.5
steps:
- name: Checkout repo
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Setup Windows builder
run: |
@ -310,8 +320,10 @@ jobs:
env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
steps:
- name: Checkout repo
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Print environment
run: |
@ -386,10 +398,14 @@ jobs:
- cli
- cli-windows
- snap
- check-run
steps:
- name: Check if any job failed
working-directory: ${{ github.workspace }}
if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') && contains(needs.*.result, 'failure')
if: |
github.event_name != 'pull_request_target'
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
&& contains(needs.*.result, 'failure')
run: exit 1
- name: Login to Azure - Prod Subscription

View File

@ -1,7 +1,8 @@
name: Build Desktop
on:
pull_request:
pull_request_target:
types: [opened, synchronize]
branches-ignore:
- 'l10n_master'
- 'cf-pages'
@ -32,12 +33,18 @@ defaults:
shell: bash
jobs:
check-run:
name: Check PR run
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
electron-verify:
name: Verify Electron Version
runs-on: ubuntu-22.04
steps:
- name: Checkout repo
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Verify
run: |
@ -65,8 +72,10 @@ jobs:
run:
working-directory: apps/desktop
steps:
- name: Checkout repo
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Get Package Version
id: retrieve-version
@ -138,8 +147,10 @@ jobs:
run:
working-directory: apps/desktop
steps:
- name: Checkout repo
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
@ -238,7 +249,9 @@ jobs:
windows:
name: Windows Build
runs-on: windows-2022
needs: setup
needs:
- setup
- check-run
defaults:
run:
shell: pwsh
@ -248,8 +261,10 @@ jobs:
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
NODE_OPTIONS: --max_old_space_size=4096
steps:
- name: Checkout repo
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
@ -447,7 +462,9 @@ jobs:
macos-build:
name: MacOS Build
runs-on: macos-13
needs: setup
needs:
- setup
- check-run
env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
@ -456,8 +473,10 @@ jobs:
run:
working-directory: apps/desktop
steps:
- name: Checkout repo
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
@ -622,8 +641,10 @@ jobs:
run:
working-directory: apps/desktop
steps:
- name: Checkout repo
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
@ -841,8 +862,10 @@ jobs:
run:
working-directory: apps/desktop
steps:
- name: Checkout repo
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
@ -1088,8 +1111,10 @@ jobs:
run:
working-directory: apps/desktop
steps:
- name: Checkout repo
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
@ -1279,8 +1304,10 @@ jobs:
- macos-package-mas
runs-on: ubuntu-22.04
steps:
- name: Checkout repo
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Login to Azure
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
@ -1323,7 +1350,10 @@ jobs:
- crowdin-push
steps:
- name: Check if any job failed
if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') && contains(needs.*.result, 'failure')
if: |
github.event_name != 'pull_request_target'
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
&& contains(needs.*.result, 'failure')
run: exit 1
- name: Login to Azure - Prod Subscription

View File

@ -1,7 +1,8 @@
name: Build Web
on:
pull_request:
pull_request_target:
types: [opened, synchronize]
branches-ignore:
- 'l10n_master'
- 'cf-pages'
@ -36,6 +37,10 @@ env:
_AZ_REGISTRY: bitwardenprod.azurecr.io
jobs:
check-run:
name: Check PR run
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
setup:
name: Setup
runs-on: ubuntu-22.04
@ -43,8 +48,10 @@ jobs:
version: ${{ steps.version.outputs.value }}
node_version: ${{ steps.retrieve-node-version.outputs.node_version }}
steps:
- name: Checkout repo
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Get GitHub sha as version
id: version
@ -89,8 +96,10 @@ jobs:
git_metadata: true
steps:
- name: Checkout repo
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
@ -142,6 +151,7 @@ jobs:
needs:
- setup
- build-artifacts
- check-run
strategy:
fail-fast: false
matrix:
@ -155,8 +165,10 @@ jobs:
env:
_VERSION: ${{ needs.setup.outputs.version }}
steps:
- name: Checkout repo
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Check Branch to Publish
env:
@ -250,11 +262,15 @@ jobs:
crowdin-push:
name: Crowdin Push
if: github.ref == 'refs/heads/main'
needs: build-artifacts
needs:
- build-artifacts
- check-run
runs-on: ubuntu-22.04
steps:
- name: Checkout repo
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Login to Azure
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
@ -282,9 +298,11 @@ jobs:
trigger-web-vault-deploy:
name: Trigger web vault deploy
if: github.ref == 'refs/heads/main'
if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main'
runs-on: ubuntu-22.04
needs: build-artifacts
needs:
- build-artifacts
- check-run
steps:
- name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
@ -326,7 +344,10 @@ jobs:
- trigger-web-vault-deploy
steps:
- name: Check if any job failed
if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') && contains(needs.*.result, 'failure')
if: |
github.event_name != 'pull_request_target'
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
&& contains(needs.*.result, 'failure')
run: exit 1
- name: Login to Azure - Prod Subscription

View File

@ -1,124 +1,130 @@
name: Version Bump
name: Repository management
on:
workflow_dispatch:
inputs:
task:
default: "Version Bump"
description: "Task to execute"
options:
- "Version Bump"
- "Version Bump and Cut rc"
required: true
type: choice
bump_browser:
description: "Bump Browser?"
description: "Bump Browser version?"
type: boolean
default: false
bump_cli:
description: "Bump CLI?"
description: "Bump CLI version?"
type: boolean
default: false
bump_desktop:
description: "Bump Desktop?"
description: "Bump Desktop version?"
type: boolean
default: false
bump_web:
description: "Bump Web?"
description: "Bump Web version?"
type: boolean
default: false
target_ref:
default: "main"
description: "Branch/Tag to target for cut"
required: true
type: string
version_number_override:
description: "New version override (leave blank for automatic calculation, example: '2024.1.0')"
required: false
type: string
cut_rc_branch:
description: "Cut RC branch?"
default: true
type: boolean
enable_slack_notification:
description: "Enable Slack notifications for upcoming release?"
default: false
type: boolean
jobs:
setup:
name: Setup
runs-on: ubuntu-24.04
outputs:
branch: ${{ steps.set-branch.outputs.branch }}
token: ${{ steps.app-token.outputs.token }}
steps:
- name: Set branch
id: set-branch
env:
TASK: ${{ inputs.task }}
run: |
if [[ "$TASK" == "Version Bump" ]]; then
BRANCH="none"
elif [[ "$TASK" == "Version Bump and Cut rc" ]]; then
BRANCH="rc"
fi
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
- name: Generate GH App token
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
id: app-token
with:
app-id: ${{ secrets.BW_GHAPP_ID }}
private-key: ${{ secrets.BW_GHAPP_KEY }}
cut_branch:
name: Cut branch
if: ${{ needs.setup.outputs.branch == 'rc' }}
needs: setup
runs-on: ubuntu-24.04
steps:
- name: Check out target ref
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ inputs.target_ref }}
token: ${{ needs.setup.outputs.token }}
- name: Check if ${{ needs.setup.outputs.branch }} branch exists
env:
BRANCH_NAME: ${{ needs.setup.outputs.branch }}
run: |
if [[ $(git ls-remote --heads origin $BRANCH_NAME) ]]; then
echo "$BRANCH_NAME already exists! Please delete $BRANCH_NAME before running again." >> $GITHUB_STEP_SUMMARY
exit 1
fi
- name: Cut branch
env:
BRANCH_NAME: ${{ needs.setup.outputs.branch }}
run: |
git switch --quiet --create $BRANCH_NAME
git push --quiet --set-upstream origin $BRANCH_NAME
bump_version:
name: Bump Version
runs-on: ubuntu-22.04
if: ${{ always() }}
runs-on: ubuntu-24.04
needs:
- cut_branch
- setup
outputs:
version_browser: ${{ steps.set-final-version-output.outputs.version_browser }}
version_cli: ${{ steps.set-final-version-output.outputs.version_cli }}
version_desktop: ${{ steps.set-final-version-output.outputs.version_desktop }}
version_web: ${{ steps.set-final-version-output.outputs.version_web }}
steps:
- name: Validate version input
- name: Validate version input format
if: ${{ inputs.version_number_override != '' }}
uses: bitwarden/gh-actions/version-check@main
with:
version: ${{ inputs.version_number_override }}
- name: Slack Notification Check
run: |
if [[ "${{ inputs.enable_slack_notification }}" == true ]]; then
echo "Slack notifications enabled."
else
echo "Slack notifications disabled."
fi
- name: Checkout Branch
- name: Check out branch
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: main
token: ${{ needs.setup.outputs.token }}
- name: Check if RC branch exists
if: ${{ inputs.cut_rc_branch == true }}
- name: Configure Git
run: |
remote_rc_branch_check=$(git ls-remote --heads origin rc | wc -l)
if [[ "${remote_rc_branch_check}" -gt 0 ]]; then
echo "Remote RC branch exists."
echo "Please delete current RC branch before running again."
exit 1
fi
- name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Retrieve secrets
id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: "bitwarden-ci"
secrets: "github-gpg-private-key,
github-gpg-private-key-passphrase"
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@cb9bde2e2525e640591a934b1fd28eef1dcaf5e5 # v6.2.0
with:
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}
git_user_signingkey: true
git_commit_gpgsign: true
- name: Setup git
run: |
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
git config --local user.name "bitwarden-devops-bot"
- name: Create Version Branch
id: create-branch
run: |
CLIENTS=()
if [[ ${{ inputs.bump_browser }} == true ]]; then
CLIENTS+=("browser")
fi
if [[ ${{ inputs.bump_cli }} == true ]]; then
CLIENTS+=("cli")
fi
if [[ ${{ inputs.bump_desktop }} == true ]]; then
CLIENTS+=("desktop")
fi
if [[ ${{ inputs.bump_web }} == true ]]; then
CLIENTS+=("web")
fi
printf -v joined '%s,' "${CLIENTS[@]}"
echo "client=${joined%,}" >> $GITHUB_OUTPUT
NAME=version_bump_${{ github.ref_name }}_$(date +"%Y-%m-%d")
git switch -c $NAME
echo "name=$NAME" >> $GITHUB_OUTPUT
git config --local user.email "actions@github.com"
git config --local user.name "Github Actions"
########################
# VERSION BUMP SECTION #
@ -165,7 +171,9 @@ jobs:
- name: Bump Browser Version - Version Override
if: ${{ inputs.bump_browser == true && inputs.version_number_override != '' }}
id: bump-browser-version-override
run: npm version --workspace=@bitwarden/browser ${{ inputs.version_number_override }}
env:
VERSION: ${{ inputs.version_number_override }}
run: npm version --workspace=@bitwarden/browser $VERSION
- name: Bump Browser Version - Automatic Calculation
if: ${{ inputs.bump_browser == true && inputs.version_number_override == '' }}
@ -250,7 +258,9 @@ jobs:
- name: Bump CLI Version - Version Override
if: ${{ inputs.bump_cli == true && inputs.version_number_override != '' }}
id: bump-cli-version-override
run: npm version --workspace=@bitwarden/cli ${{ inputs.version_number_override }}
env:
VERSION: ${{ inputs.version_number_override }}
run: npm version --workspace=@bitwarden/cli $VERSION
- name: Bump CLI Version - Automatic Calculation
if: ${{ inputs.bump_cli == true && inputs.version_number_override == '' }}
@ -300,7 +310,9 @@ jobs:
- name: Bump Desktop Version - Root - Version Override
if: ${{ inputs.bump_desktop == true && inputs.version_number_override != '' }}
id: bump-desktop-version-override
run: npm version --workspace=@bitwarden/desktop ${{ inputs.version_number_override }}
env:
VERSION: ${{ inputs.version_number_override }}
run: npm version --workspace=@bitwarden/desktop $VERSION
- name: Bump Desktop Version - Root - Automatic Calculation
if: ${{ inputs.bump_desktop == true && inputs.version_number_override == '' }}
@ -311,7 +323,9 @@ jobs:
- name: Bump Desktop Version - App - Version Override
if: ${{ inputs.bump_desktop == true && inputs.version_number_override != '' }}
run: npm version ${{ inputs.version_number_override }}
env:
VERSION: ${{ inputs.version_number_override }}
run: npm version $VERSION
working-directory: "apps/desktop/src"
- name: Bump Desktop Version - App - Automatic Calculation
@ -362,7 +376,9 @@ jobs:
- name: Bump Web Version - Version Override
if: ${{ inputs.bump_web == true && inputs.version_number_override != '' }}
id: bump-web-version-override
run: npm version --workspace=@bitwarden/web-vault ${{ inputs.version_number_override }}
env:
VERSION: ${{ inputs.version_number_override }}
run: npm version --workspace=@bitwarden/web-vault $VERSION
- name: Bump Web Version - Automatic Calculation
if: ${{ inputs.bump_web == true && inputs.version_number_override == '' }}
@ -375,27 +391,29 @@ jobs:
- name: Set final version output
id: set-final-version-output
env:
VERSION: ${{ inputs.version_number_override }}
run: |
if [[ "${{ steps.bump-browser-version-override.outcome }}" = "success" ]]; then
echo "version_browser=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT
echo "version_browser=$VERSION" >> $GITHUB_OUTPUT
elif [[ "${{ steps.bump-browser-version-automatic.outcome }}" = "success" ]]; then
echo "version_browser=${{ steps.calculate-next-browser-version.outputs.version }}" >> $GITHUB_OUTPUT
fi
if [[ "${{ steps.bump-cli-version-override.outcome }}" = "success" ]]; then
echo "version_cli=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT
echo "version_cli=$VERSION" >> $GITHUB_OUTPUT
elif [[ "${{ steps.bump-cli-version-automatic.outcome }}" = "success" ]]; then
echo "version_cli=${{ steps.calculate-next-cli-version.outputs.version }}" >> $GITHUB_OUTPUT
fi
if [[ "${{ steps.bump-desktop-version-override.outcome }}" = "success" ]]; then
echo "version_desktop=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT
echo "version_desktop=$VERSION" >> $GITHUB_OUTPUT
elif [[ "${{ steps.bump-desktop-version-automatic.outcome }}" = "success" ]]; then
echo "version_desktop=${{ steps.calculate-next-desktop-version.outputs.version }}" >> $GITHUB_OUTPUT
fi
if [[ "${{ steps.bump-web-version-override.outcome }}" = "success" ]]; then
echo "version_web=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT
echo "version_web=$VERSION" >> $GITHUB_OUTPUT
elif [[ "${{ steps.bump-web-version-automatic.outcome }}" = "success" ]]; then
echo "version_web=${{ steps.calculate-next-web-version.outputs.version }}" >> $GITHUB_OUTPUT
fi
@ -416,199 +434,52 @@ jobs:
- name: Push changes
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
env:
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
run: git push -u origin $PR_BRANCH
run: git push
- name: Generate PR message
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
id: pr-message
run: |
MESSAGE=""
if [[ "${{ inputs.bump_browser }}" == "true" ]]; then
MESSAGE+=$' Browser version bump to ${{ steps.set-final-version-output.outputs.version_browser }}\n'
fi
if [[ "${{ inputs.bump_cli }}" == "true" ]]; then
MESSAGE+=$' CLI version bump to ${{ steps.set-final-version-output.outputs.version_cli }}\n'
fi
if [[ "${{ inputs.bump_desktop }}" == "true" ]]; then
MESSAGE+=$' Desktop version bump to ${{ steps.set-final-version-output.outputs.version_desktop }}\n'
fi
if [[ "${{ inputs.bump_web }}" == "true" ]]; then
MESSAGE+=$' Web version bump to ${{ steps.set-final-version-output.outputs.version_web }}\n'
fi
echo "MESSAGE<<EOF" >> $GITHUB_ENV
echo "$MESSAGE" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Generate GH App token
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
id: app-token
with:
app-id: ${{ secrets.BW_GHAPP_ID }}
private-key: ${{ secrets.BW_GHAPP_KEY }}
owner: ${{ github.repository_owner }}
- name: Create Version PR
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
id: create-pr
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
TITLE: "Bump client(s) version"
run: |
PR_URL=$(gh pr create --title "$TITLE" \
--base "main" \
--head "$PR_BRANCH" \
--label "version update" \
--label "automated pr" \
--body "
## Type of change
- [ ] Bug fix
- [ ] New feature development
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
- [ ] Build/deploy pipeline (DevOps)
- [X] Other
## Objective
$MESSAGE")
echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT
- name: Approve PR
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
run: gh pr review $PR_NUMBER --approve
- name: Merge PR
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
run: gh pr merge $PR_NUMBER --squash --auto --delete-branch
- name: Report upcoming browser release version to Slack
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' && steps.set-final-version-output.outputs.version_browser != '' && inputs.enable_slack_notification == true }}
uses: bitwarden/gh-actions/report-upcoming-release-version@main
with:
version: ${{ steps.set-final-version-output.outputs.version_browser }}
project: browser
AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Report upcoming cli release version to Slack
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' && steps.set-final-version-output.outputs.version_cli != '' && inputs.enable_slack_notification == true }}
uses: bitwarden/gh-actions/report-upcoming-release-version@main
with:
version: ${{ steps.set-final-version-output.outputs.version_cli }}
project: cli
AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Report upcoming desktop release version to Slack
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' && steps.set-final-version-output.outputs.version_desktop != '' && inputs.enable_slack_notification == true }}
uses: bitwarden/gh-actions/report-upcoming-release-version@main
with:
version: ${{ steps.set-final-version-output.outputs.version_desktop }}
project: desktop
AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Report upcoming web release version to Slack
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' && steps.set-final-version-output.outputs.version_web != '' && inputs.enable_slack_notification == true }}
uses: bitwarden/gh-actions/report-upcoming-release-version@main
with:
version: ${{ steps.set-final-version-output.outputs.version_web }}
project: web
AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
cut_rc:
name: Cut RC branch
if: ${{ inputs.cut_rc_branch == true }}
needs: bump_version
runs-on: ubuntu-22.04
cherry_pick:
name: Cherry-Pick Commit(s)
if: ${{ needs.setup.outputs.branch == 'rc' }}
runs-on: ubuntu-24.04
needs:
- bump_version
- setup
steps:
- name: Checkout Branch
- name: Check out main branch
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: main
token: ${{ needs.setup.outputs.token }}
### Browser
- name: Browser - Verify version has been updated
if: ${{ inputs.bump_browser == true }}
env:
NEW_VERSION: ${{ needs.bump_version.outputs.version_browser }}
- name: Configure Git
run: |
# Wait for version to change.
while : ; do
echo "Waiting for version to be updated..."
git pull --force
CURRENT_VERSION=$(cat package.json | jq -r '.version')
git config --local user.email "actions@github.com"
git config --local user.name "Github Actions"
# If the versions don't match we continue the loop, otherwise we break out of the loop.
[[ "$NEW_VERSION" != "$CURRENT_VERSION" ]] || break
sleep 10
done
working-directory: apps/browser
### CLI
- name: CLI - Verify version has been updated
if: ${{ inputs.bump_cli == true }}
env:
NEW_VERSION: ${{ needs.bump_version.outputs.version_cli }}
- name: Perform cherry-pick(s)
run: |
# Wait for version to change.
while : ; do
echo "Waiting for version to be updated..."
git pull --force
CURRENT_VERSION=$(cat package.json | jq -r '.version')
# Function for cherry-picking
cherry_pick () {
local package_path="apps/$1/package.json"
local source_branch=$2
local destination_branch=$3
# If the versions don't match we continue the loop, otherwise we break out of the loop.
[[ "$NEW_VERSION" != "$CURRENT_VERSION" ]] || break
sleep 10
done
working-directory: apps/cli
# Get project commit/version from source branch
git switch $source_branch
SOURCE_COMMIT=$(git log --reverse --pretty=format:"%H" --max-count=1 $package_path)
SOURCE_VERSION=$(cat $package_path | jq -r '.version')
### Desktop
- name: Desktop - Verify version has been updated
if: ${{ inputs.bump_desktop == true }}
env:
NEW_VERSION: ${{ needs.bump_version.outputs.version_desktop }}
run: |
# Wait for version to change.
while : ; do
echo "Waiting for version to be updated..."
git pull --force
CURRENT_VERSION=$(cat package.json | jq -r '.version')
# Get project commit/version from destination branch
git switch $destination_branch
DESTINATION_VERSION=$(cat $package_path | jq -r '.version')
# If the versions don't match we continue the loop, otherwise we break out of the loop.
[[ "$NEW_VERSION" != "$CURRENT_VERSION" ]] || break
sleep 10
done
working-directory: apps/desktop
if [[ "$DESTINATION_VERSION" != "$SOURCE_VERSION" ]]; then
git cherry-pick --strategy-option=theirs -x $SOURCE_COMMIT
git push -u origin $destination_branch
fi
### Web
- name: Web - Verify version has been updated
if: ${{ inputs.bump_web == true }}
env:
NEW_VERSION: ${{ needs.bump_version.outputs.version_web }}
run: |
# Wait for version to change.
while : ; do
echo "Waiting for version to be updated..."
git pull --force
CURRENT_VERSION=$(cat package.json | jq -r '.version')
# If the versions don't match we continue the loop, otherwise we break out of the loop.
[[ "$NEW_VERSION" != "$CURRENT_VERSION" ]] || break
sleep 10
done
working-directory: apps/web
- name: Cut RC branch
run: |
git switch --quiet --create rc
git push --quiet --set-upstream origin rc
# Cherry-pick from 'main' into 'rc'
cherry_pick browser main rc
cherry_pick cli main rc
cherry_pick desktop main rc
cherry_pick web main rc

View File

@ -8,27 +8,55 @@ on:
jobs:
bump-version:
name: Bump Desktop Version
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
- name: Generate GH App token
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
id: app-token
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
app-id: ${{ secrets.BW_GHAPP_ID }}
private-key: ${{ secrets.BW_GHAPP_KEY }}
- name: Retrieve bot secrets
id: retrieve-bot-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
- name: Check out target ref
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
keyvault: bitwarden-ci
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
ref: main
token: ${{ steps.app-token.outputs.token }}
- name: Trigger Version Bump workflow
env:
GH_TOKEN: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
- name: Configure Git
run: |
echo '{"cut_rc_branch": "false",
"bump_browser": "false",
"bump_cli": "false",
"bump_desktop": "true",
"bump_web": "false"}' | \
gh workflow run version-bump.yml --json --repo bitwarden/clients
git config --local user.email "actions@github.com"
git config --local user.name "Github Actions"
- name: Get current Desktop version
id: current-desktop-version
run: |
CURRENT_VERSION=$(cat package.json | jq -r '.version')
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
working-directory: apps/desktop
- name: Calculate next Desktop release version
id: calculate-next-desktop-version
uses: bitwarden/gh-actions/version-next@main
with:
version: ${{ steps.current-desktop-version.outputs.version }}
- name: Bump Desktop Version - Root - Automatic Calculation
id: bump-desktop-version-automatic
env:
VERSION: ${{ steps.calculate-next-desktop-version.outputs.version }}
run: npm version --workspace=@bitwarden/desktop $VERSION
- name: Bump Desktop Version - App - Automatic Calculation
env:
VERSION: ${{ steps.calculate-next-desktop-version.outputs.version }}
run: npm version $VERSION
working-directory: "apps/desktop/src"
- name: Commit files
env:
VERSION: ${{ steps.calculate-next-desktop-version.outputs.version }}
run: git commit -m "Bumped Desktop client to $VERSION" -a
- name: Push changes
run: git push

View File

@ -128,10 +128,10 @@
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "test-storybook:build:production"
"buildTarget": "test-storybook:build:production"
},
"development": {
"browserTarget": "test-storybook:build:development"
"buildTarget": "test-storybook:build:development"
}
},
"defaultConfiguration": "development"

View File

@ -9,7 +9,6 @@ const replace = require("gulp-replace");
const manifest = require("./src/manifest.json");
const manifestVersion = parseInt(process.env.MANIFEST_VERSION || manifest.version);
const betaBuild = process.env.BETA_BUILD === "1";
const paths = {
build: "./build/",
@ -17,27 +16,11 @@ const paths = {
safari: "./src/safari/",
};
/**
* Converts a number to a tuple containing two Uint16's
* @param num {number} This number is expected to be a integer style number with no decimals
*
* @returns {number[]} A tuple containing two elements that are both numbers.
*/
function numToUint16s(num) {
var arr = new ArrayBuffer(4);
var view = new DataView(arr);
view.setUint32(0, num, false);
return [view.getUint16(0), view.getUint16(2)];
}
function buildString() {
var build = "";
if (process.env.MANIFEST_VERSION) {
build = `-mv${process.env.MANIFEST_VERSION}`;
}
if (betaBuild) {
build += "-beta";
}
if (process.env.BUILD_NUMBER && process.env.BUILD_NUMBER !== "") {
build = `-${process.env.BUILD_NUMBER}`;
}
@ -71,9 +54,6 @@ function distFirefox() {
manifest.optional_permissions = manifest.optional_permissions.filter(
(permission) => permission !== "privacy",
);
if (betaBuild) {
manifest = applyBetaLabels(manifest);
}
return manifest;
});
}
@ -90,9 +70,6 @@ function distOpera() {
delete manifest.commands._execute_sidebar_action;
}
if (betaBuild) {
manifest = applyBetaLabels(manifest);
}
return manifest;
});
}
@ -102,9 +79,6 @@ function distChrome() {
delete manifest.applications;
delete manifest.sidebar_action;
delete manifest.commands._execute_sidebar_action;
if (betaBuild) {
manifest = applyBetaLabels(manifest);
}
return manifest;
});
}
@ -114,9 +88,6 @@ function distEdge() {
delete manifest.applications;
delete manifest.sidebar_action;
delete manifest.commands._execute_sidebar_action;
if (betaBuild) {
manifest = applyBetaLabels(manifest);
}
return manifest;
});
}
@ -237,9 +208,6 @@ async function safariCopyBuild(source, dest) {
delete manifest.commands._execute_sidebar_action;
delete manifest.optional_permissions;
manifest.permissions.push("nativeMessaging");
if (betaBuild) {
manifest = applyBetaLabels(manifest);
}
return manifest;
}),
),
@ -254,30 +222,6 @@ function stdOutProc(proc) {
proc.stderr.on("data", (data) => console.error(data.toString()));
}
function applyBetaLabels(manifest) {
manifest.name = "Bitwarden Password Manager BETA";
manifest.short_name = "Bitwarden BETA";
manifest.description = "THIS EXTENSION IS FOR BETA TESTING BITWARDEN.";
if (process.env.GITHUB_RUN_ID) {
const existingVersionParts = manifest.version.split("."); // 3 parts expected 2024.4.0
// GITHUB_RUN_ID is a number like: 8853654662
// which will convert to [ 4024, 3206 ]
// and a single incremented id of 8853654663 will become [ 4024, 3207 ]
const runIdParts = numToUint16s(parseInt(process.env.GITHUB_RUN_ID));
// Only use the first 2 parts from the given version number and base the other 2 numbers from the GITHUB_RUN_ID
// Example: 2024.4.4024.3206
const betaVersion = `${existingVersionParts[0]}.${existingVersionParts[1]}.${runIdParts[0]}.${runIdParts[1]}`;
manifest.version_name = `${betaVersion} beta - ${process.env.GITHUB_SHA.slice(0, 8)}`;
manifest.version = betaVersion;
} else {
manifest.version = `${manifest.version}.0`;
}
return manifest;
}
exports["dist:firefox"] = distFirefox;
exports["dist:chrome"] = distChrome;
exports["dist:opera"] = distOpera;

View File

@ -1,6 +1,6 @@
{
"name": "@bitwarden/browser",
"version": "2024.10.1",
"version": "2024.11.0",
"scripts": {
"build": "cross-env MANIFEST_VERSION=3 webpack",
"build:mv2": "webpack",
@ -10,12 +10,9 @@
"build:watch:safari": "cross-env MANIFEST_VERSION=3 BROWSER=safari webpack --watch",
"build:watch:mv2": "webpack --watch",
"build:prod": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=4096\" webpack",
"build:prod:beta": "cross-env BETA_BUILD=1 NODE_ENV=production webpack",
"build:prod:watch": "cross-env NODE_ENV=production webpack --watch",
"dist": "npm run build:prod && gulp dist",
"dist:beta": "npm run build:prod:beta && cross-env BETA_BUILD=1 gulp dist",
"dist:mv3": "cross-env MANIFEST_VERSION=3 npm run build:prod && cross-env MANIFEST_VERSION=3 gulp dist",
"dist:mv3:beta": "cross-env MANIFEST_VERSION=3 npm run build:prod:beta && cross-env MANIFEST_VERSION=3 BETA_BUILD=1 gulp dist",
"dist:chrome": "npm run build:prod && gulp dist:chrome",
"dist:chrome:beta": "cross-env MANIFEST_VERSION=3 npm run build:prod:beta && cross-env MANIFEST_VERSION=3 BETA_BUILD=1 gulp dist:chrome",
"dist:firefox": "npm run build:prod && gulp dist:firefox",

View File

@ -2878,7 +2878,7 @@
"message": "Luo sähköpostiosoite"
},
"generatorBoundariesHint": {
"message": "Arvon tulee olla väliltä $MIN$ - $MAX$",
"message": "Arvon tulee olla väliltä $MIN$$MAX$",
"description": "Explains spin box minimum and maximum values to the user",
"placeholders": {
"min": {

View File

@ -3,7 +3,7 @@
"message": "Bitwarden"
},
"extName": {
"message": "Bitwarden Password Manager",
"message": "Bitwarden passordbehandler",
"description": "Extension name, MUST be less than 40 characters (Safari restriction)"
},
"extDesc": {
@ -29,7 +29,7 @@
"message": "Use single sign-on"
},
"welcomeBack": {
"message": "Welcome back"
"message": "Velkommen tilbake"
},
"setAStrongPassword": {
"message": "Set a strong password"
@ -168,7 +168,7 @@
"message": "Copy notes"
},
"fill": {
"message": "Fill",
"message": "Fyll",
"description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible."
},
"autoFill": {
@ -357,10 +357,10 @@
"message": "Rediger mappen"
},
"newFolder": {
"message": "New folder"
"message": "Ny mappe"
},
"folderName": {
"message": "Folder name"
"message": "Mappenavn"
},
"folderHintText": {
"message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums"
@ -458,7 +458,7 @@
"description": "deprecated. Use specialCharactersLabel instead."
},
"include": {
"message": "Include",
"message": "Inkluder",
"description": "Card header for password generator include block"
},
"uppercaseDescription": {
@ -466,7 +466,7 @@
"description": "Tooltip for the password generator uppercase character checkbox"
},
"uppercaseLabel": {
"message": "A-Z",
"message": "A-Å",
"description": "Label for the password generator uppercase character checkbox"
},
"lowercaseDescription": {
@ -478,7 +478,7 @@
"description": "Label for the password generator lowercase character checkbox"
},
"numbersDescription": {
"message": "Include numbers",
"message": "Inkluder tall",
"description": "Full description for the password generator numbers checkbox"
},
"numbersLabel": {
@ -627,7 +627,7 @@
"message": "Vault timeout"
},
"otherOptions": {
"message": "Other options"
"message": "Andre valg"
},
"rateExtension": {
"message": "Gi denne utvidelsen en vurdering"
@ -676,7 +676,7 @@
"message": "Tidsavbrudd i hvelvet"
},
"vaultTimeout1": {
"message": "Timeout"
"message": "Tidsavbrudd"
},
"lockNow": {
"message": "Lås nå"
@ -730,10 +730,10 @@
"message": "Sikkerhet"
},
"confirmMasterPassword": {
"message": "Confirm master password"
"message": "Bekreft hovedpassord"
},
"masterPassword": {
"message": "Master password"
"message": "Hovedpassord"
},
"masterPassImportant": {
"message": "Your master password cannot be recovered if you forget it!"
@ -843,7 +843,7 @@
"message": "Din innloggingsøkt har utløpt."
},
"logIn": {
"message": "Log in"
"message": "Logg inn"
},
"logInToBitwarden": {
"message": "Log in to Bitwarden"
@ -928,7 +928,7 @@
"message": "Ny URI"
},
"addDomain": {
"message": "Add domain",
"message": "Legg til domene",
"description": "'Domain' here refers to an internet domain name (e.g. 'bitwarden.com') and the message in whole described the act of putting a domain value into the context."
},
"addedItem": {
@ -1092,7 +1092,7 @@
"message": "This file export will be password protected and require the file password to decrypt."
},
"filePassword": {
"message": "File password"
"message": "Filpassord"
},
"exportPasswordDescription": {
"message": "This password will be used to export and import this file"
@ -1917,10 +1917,10 @@
"message": "Det er ingen passord å liste opp."
},
"clearHistory": {
"message": "Clear history"
"message": "Tøm historikk"
},
"nothingToShow": {
"message": "Nothing to show"
"message": "Ingenting å vise"
},
"nothingGeneratedRecently": {
"message": "You haven't generated anything recently"
@ -1984,10 +1984,10 @@
"message": "Lås opp med PIN-kode"
},
"setYourPinTitle": {
"message": "Set PIN"
"message": "Velg PIN"
},
"setYourPinButton": {
"message": "Set PIN"
"message": "Velg PIN"
},
"setYourPinCode": {
"message": "Angi PIN-koden din for å låse opp Bitwarden. PIN-innstillingene tilbakestilles hvis du logger deg helt ut av programmet."
@ -2041,7 +2041,7 @@
"message": "Username generator"
},
"useThisPassword": {
"message": "Use this password"
"message": "Bruk dette passordet"
},
"useThisUsername": {
"message": "Use this username"
@ -2186,7 +2186,7 @@
"message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox."
},
"unsubscribe": {
"message": "Unsubscribe"
"message": "Avslutt abonnement"
},
"atAnyTime": {
"message": "at any time."
@ -2195,7 +2195,7 @@
"message": "By continuing, you agree to the"
},
"and": {
"message": "and"
"message": "og"
},
"acceptPolicies": {
"message": "Ved å merke av denne boksen sier du deg enig i følgende:"
@ -2315,7 +2315,7 @@
"message": "An organization policy has blocked importing items into your individual vault."
},
"domainsTitle": {
"message": "Domains",
"message": "Domener",
"description": "A category title describing the concept of web domains"
},
"excludedDomains": {
@ -2411,7 +2411,7 @@
"message": "Passord beskyttet"
},
"copyLink": {
"message": "Copy link"
"message": "Kopier lenke"
},
"copySendLink": {
"message": "Kopier Send-lenke",
@ -2668,7 +2668,7 @@
"message": "E-postbekreftelse kreves"
},
"emailVerifiedV2": {
"message": "Email verified"
"message": "E-post bekreftet"
},
"emailVerificationRequiredDesc": {
"message": "Du må bekrefte e-posten din for å bruke denne funksjonen. Du kan bekrefte e-postadressen din i netthvelvet."
@ -3247,10 +3247,10 @@
"message": "Uncheck if using a public device"
},
"approveFromYourOtherDevice": {
"message": "Approve from your other device"
"message": "Godkjenn fra en av dine andre enheter"
},
"requestAdminApproval": {
"message": "Request admin approval"
"message": "Be om administratorgodkjennelse"
},
"approveWithMasterPassword": {
"message": "Godkjenn med hovedpassord"
@ -3274,7 +3274,7 @@
"message": "No email?"
},
"goBack": {
"message": "Go back"
"message": "Gå tilbake"
},
"toEditYourEmailAddress": {
"message": "to edit your email address."
@ -3290,7 +3290,7 @@
"message": "Generelt"
},
"display": {
"message": "Display"
"message": "Vis"
},
"accountSuccessfullyCreated": {
"message": "Account successfully created!"
@ -3416,7 +3416,7 @@
"message": "— Skriv for å filtrere —"
},
"multiSelectLoading": {
"message": "Retrieving options..."
"message": "Innhenter alternativer..."
},
"multiSelectNotFound": {
"message": "Ingen gjenstander funnet"
@ -3542,7 +3542,7 @@
"description": "Screen reader text (aria-label) for new item button in overlay"
},
"newLogin": {
"message": "New login",
"message": "Ny innlogging",
"description": "Button text to display within inline menu when there are no matching items on a login field"
},
"addNewLoginItemAria": {
@ -3558,7 +3558,7 @@
"description": "Screen reader text (aria-label) for new card button within inline menu"
},
"newIdentity": {
"message": "New identity",
"message": "Ny identitet",
"description": "Button text to display within inline menu when there are no matching items on an identity field"
},
"addNewIdentityItemAria": {
@ -3592,7 +3592,7 @@
"message": "Beskrivelse"
},
"importSuccess": {
"message": "Data successfully imported"
"message": "Dataene ble vellykket importert"
},
"importSuccessNumberOfItems": {
"message": "$AMOUNT$ gjenstander totalt ble importert.",
@ -3682,7 +3682,7 @@
"message": "Invalid file password, please use the password you entered when you created the export file."
},
"destination": {
"message": "Destination"
"message": "Destinasjon"
},
"learnAboutImportOptions": {
"message": "Lær mer om importalternativene dine"
@ -3719,7 +3719,7 @@
"message": "Ingen fil er valgt"
},
"orCopyPasteFileContents": {
"message": "or copy/paste the import file contents"
"message": "eller kopier/lim inn importfilens innhold"
},
"instructionsFor": {
"message": "$NAME$-instruksjoner",
@ -3810,7 +3810,7 @@
"message": "Multifaktorautentisering ble avbrutt"
},
"noLastPassDataFound": {
"message": "No LastPass data found"
"message": "Ingen LastPass-data ble funnet"
},
"incorrectUsernameOrPassword": {
"message": "Feil brukernavn eller passord"
@ -3837,7 +3837,7 @@
"message": "Importerer kontoen din…"
},
"lastPassMFARequired": {
"message": "LastPass multifactor authentication required"
"message": "LastPass-multifaktorautentisering kreves"
},
"lastPassMFADesc": {
"message": "Enter your one-time passcode from your authentication app"
@ -4003,7 +4003,7 @@
"description": "Notification message for when saving credentials has failed."
},
"success": {
"message": "Success"
"message": "Suksess"
},
"removePasskey": {
"message": "Remove passkey"
@ -4105,13 +4105,13 @@
"message": "Admin Console"
},
"accountSecurity": {
"message": "Account security"
"message": "Kontosikkerhet"
},
"notifications": {
"message": "Notifications"
"message": "Varsler"
},
"appearance": {
"message": "Appearance"
"message": "Utseende"
},
"errorAssigningTargetCollection": {
"message": "Error assigning target collection."
@ -4140,7 +4140,7 @@
}
},
"new": {
"message": "New"
"message": "Ny"
},
"removeItem": {
"message": "Remove $NAME$",
@ -4174,17 +4174,17 @@
"message": "Organization is deactivated"
},
"owner": {
"message": "Owner"
"message": "Eier"
},
"selfOwnershipLabel": {
"message": "You",
"message": "Du",
"description": "Used as a label to indicate that the user is the owner of an item."
},
"contactYourOrgAdmin": {
"message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance."
},
"additionalInformation": {
"message": "Additional information"
"message": "Tilleggsinformasjon"
},
"itemHistory": {
"message": "Item history"
@ -4196,13 +4196,13 @@
"message": "Owner: You"
},
"linked": {
"message": "Linked"
"message": "Tilknyttet"
},
"copySuccessful": {
"message": "Copy Successful"
},
"upload": {
"message": "Upload"
"message": "Last opp"
},
"addAttachment": {
"message": "Add attachment"
@ -4238,7 +4238,7 @@
"message": "Free organizations cannot use attachments"
},
"filters": {
"message": "Filters"
"message": "Filtre"
},
"personalDetails": {
"message": "Personal details"
@ -4247,7 +4247,7 @@
"message": "Identification"
},
"contactInfo": {
"message": "Contact info"
"message": "Kontaktinfo"
},
"downloadAttachment": {
"message": "Download - $ITEMNAME$",
@ -4263,7 +4263,7 @@
"description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher."
},
"loginCredentials": {
"message": "Login credentials"
"message": "Legitimasjoner for innlogging"
},
"authenticatorKey": {
"message": "Authenticator key"
@ -4288,10 +4288,10 @@
"message": "Website added"
},
"addWebsite": {
"message": "Add website"
"message": "Legg til nettsted"
},
"deleteWebsite": {
"message": "Delete website"
"message": "Slett nettsted"
},
"defaultLabel": {
"message": "Default ($VALUE$)",
@ -4343,16 +4343,16 @@
}
},
"enableAnimations": {
"message": "Enable animations"
"message": "Aktiver animasjoner"
},
"showAnimations": {
"message": "Show animations"
},
"addAccount": {
"message": "Add account"
"message": "Legg til konto"
},
"loading": {
"message": "Loading"
"message": "Laster"
},
"data": {
"message": "Data"
@ -4370,7 +4370,7 @@
"description": "ARIA label for the inline menu button that logs in with a passkey."
},
"assign": {
"message": "Assign"
"message": "Knytt"
},
"bulkCollectionAssignmentDialogDescriptionSingular": {
"message": "Only organization members with access to these collections will be able to see the item."
@ -4394,7 +4394,7 @@
"message": "Add field"
},
"add": {
"message": "Add"
"message": "Legg til"
},
"fieldType": {
"message": "Field type"
@ -4418,7 +4418,7 @@
"message": "Enter the the field's html id, name, aria-label, or placeholder."
},
"editField": {
"message": "Edit field"
"message": "Rediger felt"
},
"editFieldLabel": {
"message": "Edit $LABEL$",
@ -4588,13 +4588,13 @@
"message": "Show number of login autofill suggestions on extension icon"
},
"systemDefault": {
"message": "System default"
"message": "Systemforvalg"
},
"enterprisePolicyRequirementsApplied": {
"message": "Enterprise policy requirements have been applied to this setting"
},
"retry": {
"message": "Retry"
"message": "Prøv igjen"
},
"vaultCustomTimeoutMinimum": {
"message": "Minimum custom timeout is 1 minute."
@ -4624,16 +4624,16 @@
"message": "Items that have been in trash more than 30 days will automatically be deleted"
},
"restore": {
"message": "Restore"
"message": "Gjenopprett"
},
"deleteForever": {
"message": "Delete forever"
"message": "Slett for alltid"
},
"noEditPermissions": {
"message": "You don't have permission to edit this item"
},
"authenticating": {
"message": "Authenticating"
"message": "Autentiserer"
},
"fillGeneratedPassword": {
"message": "Fill generated password",
@ -4648,7 +4648,7 @@
"description": "Confirmation message for saving a login to Bitwarden"
},
"spaceCharacterDescriptor": {
"message": "Space",
"message": "Mellomrom",
"description": "Represents the space key in screen reader content as a readable word"
},
"tildeCharacterDescriptor": {
@ -4656,15 +4656,15 @@
"description": "Represents the ~ key in screen reader content as a readable word"
},
"backtickCharacterDescriptor": {
"message": "Backtick",
"message": "Baklengs apostrof",
"description": "Represents the ` key in screen reader content as a readable word"
},
"exclamationCharacterDescriptor": {
"message": "Exclamation mark",
"message": "Utropstegn",
"description": "Represents the ! key in screen reader content as a readable word"
},
"atSignCharacterDescriptor": {
"message": "At sign",
"message": "Alfakrøll",
"description": "Represents the @ key in screen reader content as a readable word"
},
"hashSignCharacterDescriptor": {
@ -4672,7 +4672,7 @@
"description": "Represents the # key in screen reader content as a readable word"
},
"dollarSignCharacterDescriptor": {
"message": "Dollar sign",
"message": "Dollartegn",
"description": "Represents the $ key in screen reader content as a readable word"
},
"percentSignCharacterDescriptor": {
@ -4684,7 +4684,7 @@
"description": "Represents the ^ key in screen reader content as a readable word"
},
"ampersandCharacterDescriptor": {
"message": "Ampersand",
"message": "Prosenttegn",
"description": "Represents the & key in screen reader content as a readable word"
},
"asteriskCharacterDescriptor": {
@ -4700,7 +4700,7 @@
"description": "Represents the ) key in screen reader content as a readable word"
},
"hyphenCharacterDescriptor": {
"message": "Underscore",
"message": "Understrek",
"description": "Represents the _ key in screen reader content as a readable word"
},
"underscoreCharacterDescriptor": {
@ -4740,11 +4740,11 @@
"description": "Represents the back slash key in screen reader content as a readable word"
},
"colonCharacterDescriptor": {
"message": "Colon",
"message": "Kolon",
"description": "Represents the : key in screen reader content as a readable word"
},
"semicolonCharacterDescriptor": {
"message": "Semicolon",
"message": "Semikolon",
"description": "Represents the ; key in screen reader content as a readable word"
},
"doubleQuoteCharacterDescriptor": {
@ -4756,7 +4756,7 @@
"description": "Represents the ' key in screen reader content as a readable word"
},
"lessThanCharacterDescriptor": {
"message": "Less than",
"message": "Mindre enn",
"description": "Represents the < key in screen reader content as a readable word"
},
"greaterThanCharacterDescriptor": {
@ -4764,7 +4764,7 @@
"description": "Represents the > key in screen reader content as a readable word"
},
"commaCharacterDescriptor": {
"message": "Comma",
"message": "Komma",
"description": "Represents the , key in screen reader content as a readable word"
},
"periodCharacterDescriptor": {
@ -4772,7 +4772,7 @@
"description": "Represents the . key in screen reader content as a readable word"
},
"questionCharacterDescriptor": {
"message": "Question mark",
"message": "Spørsmålstegn",
"description": "Represents the ? key in screen reader content as a readable word"
},
"forwardSlashCharacterDescriptor": {
@ -4780,10 +4780,10 @@
"description": "Represents the / key in screen reader content as a readable word"
},
"lowercaseAriaLabel": {
"message": "Lowercase"
"message": "Små bokstaver"
},
"uppercaseAriaLabel": {
"message": "Uppercase"
"message": "Store bokstaver"
},
"generatedPassword": {
"message": "Generated password"

View File

@ -20,16 +20,16 @@
"message": "Креирај налог"
},
"newToBitwarden": {
"message": "New to Bitwarden?"
"message": "Нови сте у Bitwarden-у?"
},
"logInWithPasskey": {
"message": "Log in with passkey"
"message": "Пријавите се са приступним кључем"
},
"useSingleSignOn": {
"message": "Use single sign-on"
"message": "Употребити једнократну пријаву"
},
"welcomeBack": {
"message": "Welcome back"
"message": "Добродошли назад"
},
"setAStrongPassword": {
"message": "Поставите јаку лозинку"
@ -120,7 +120,7 @@
"message": "Копирај лозинку"
},
"copyPassphrase": {
"message": "Copy passphrase"
"message": "Копирај приступну фразу"
},
"copyNote": {
"message": "Копирај белешку"
@ -168,7 +168,7 @@
"message": "Копирати белешке"
},
"fill": {
"message": "Fill",
"message": "Попуни",
"description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible."
},
"autoFill": {
@ -427,7 +427,7 @@
"message": "Генерисање лозинке"
},
"generatePassphrase": {
"message": "Generate passphrase"
"message": "Генеришите приступну фразу"
},
"regeneratePassword": {
"message": "Поново генериши лозинку"
@ -591,7 +591,7 @@
"message": "Покрените веб локацију"
},
"launchWebsiteName": {
"message": "Launch website $ITEMNAME$",
"message": "Покренути сајт $ITEMNAME$",
"placeholders": {
"itemname": {
"content": "$1",
@ -846,7 +846,7 @@
"message": "Пријави се"
},
"logInToBitwarden": {
"message": "Log in to Bitwarden"
"message": "Пријавите се на Bitwarden"
},
"restartRegistration": {
"message": "Поново покрените регистрацију"
@ -1424,7 +1424,7 @@
"message": "УРЛ Сервера"
},
"selfHostBaseUrl": {
"message": "Self-host server URL",
"message": "УРЛ сервера који се самостално хостује",
"description": "Label for field requesting a self-hosted integration service URL"
},
"apiUrl": {
@ -1795,13 +1795,13 @@
"message": "Историја Лозинке"
},
"generatorHistory": {
"message": "Generator history"
"message": "Генератор историје"
},
"clearGeneratorHistoryTitle": {
"message": "Clear generator history"
"message": "Испразнити генератор историје"
},
"cleargGeneratorHistoryDescription": {
"message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?"
"message": "Ако наставите, сви уноси ће бити трајно избрисани из генератора историје. Да ли сте сигурни да желите да наставите?"
},
"back": {
"message": "Назад"
@ -1920,10 +1920,10 @@
"message": "Обриши историју"
},
"nothingToShow": {
"message": "Nothing to show"
"message": "Нема шта да се прикаже"
},
"nothingGeneratedRecently": {
"message": "You haven't generated anything recently"
"message": "Недавно нисте ништа генерисали"
},
"remove": {
"message": "Уклони"
@ -2183,7 +2183,7 @@
"message": "Ваша нова главна лозинка не испуњава захтеве смерница."
},
"receiveMarketingEmailsV2": {
"message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox."
"message": "Добијајте савете, најаве и могућности истраживања од Bitwarden-а у пријемном сандучету."
},
"unsubscribe": {
"message": "Одјави се"
@ -2512,7 +2512,7 @@
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendPasswordDescV3": {
"message": "Add an optional password for recipients to access this Send.",
"message": "Додајте опционалну лозинку за примаоце да приступе овом Send.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendNotesDesc": {
@ -2875,10 +2875,10 @@
"message": "Генериши име"
},
"generateEmail": {
"message": "Generate email"
"message": "Генеришите имејл"
},
"generatorBoundariesHint": {
"message": "Value must be between $MIN$ and $MAX$",
"message": "Вредност мора бити између $MIN$ и $MAX$",
"description": "Explains spin box minimum and maximum values to the user",
"placeholders": {
"min": {
@ -2932,11 +2932,11 @@
"message": "Генеришите псеудоним е-поште помоћу екстерне услуге прослеђивања."
},
"forwarderDomainName": {
"message": "Email domain",
"message": "Домен имејла",
"description": "Labels the domain name email forwarder service option"
},
"forwarderDomainNameHint": {
"message": "Choose a domain that is supported by the selected service",
"message": "Изаберите домен који подржава изабрана услуга",
"description": "Guidance provided for email forwarding services that support multiple email domains."
},
"forwarderError": {
@ -3664,7 +3664,7 @@
"message": "Искачући додатак да бисте довршили пријаву."
},
"popoutExtension": {
"message": "Popout extension"
"message": "Искачући додатак"
},
"launchDuo": {
"message": "Покренути DUO"
@ -3744,25 +3744,25 @@
"message": "Подаци из сефа су извезени"
},
"typePasskey": {
"message": "Приступачни кључ"
"message": "Приступни кључ"
},
"accessing": {
"message": "Приступ"
},
"passkeyNotCopied": {
"message": "Приступачни кључ неће бити копиран"
"message": "Приступни кључ неће бити копиран"
},
"passkeyNotCopiedAlert": {
"message": "Приступачни кључ неће бити копиран на клонирану ставку. Да ли желите да наставите са клонирањем ставке?"
"message": "Приступни кључ неће бити копиран на клонирану ставку. Да ли желите да наставите са клонирањем ставке?"
},
"passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": {
"message": "Верификацију захтева сајт који покреће. Ова функција још увек није имплементирана за налоге без главне лозинке."
},
"logInWithPasskeyQuestion": {
"message": "Пријавите се са приступачним кључем?"
"message": "Пријавите се са приступним кључем?"
},
"passkeyAlreadyExists": {
"message": "За ову апликацију већ постоји приступачни кључ."
"message": "За ову апликацију већ постоји приступни кључ."
},
"noPasskeysFoundForThisApplication": {
"message": "Нису пронађени приступни кључеви за ову апликацију."
@ -3792,7 +3792,7 @@
"message": "Изаберите приступни кључ за пријаву"
},
"passkeyItem": {
"message": "Ставка приступачног кључа"
"message": "Ставка приступног кључа"
},
"overwritePasskey": {
"message": "Заменити приступни кључ?"
@ -3910,7 +3910,7 @@
"message": "сервер"
},
"hostedAt": {
"message": "hosted at"
"message": "хостиран на"
},
"useDeviceOrHardwareKey": {
"message": "Користите свој уређај или хардверски кључ"
@ -4006,10 +4006,10 @@
"message": "Успех"
},
"removePasskey": {
"message": "Уклонити приступачни кључ"
"message": "Уклонити приступни кључ"
},
"passkeyRemoved": {
"message": "Приступачни кључ је уклоњен"
"message": "Приступни кључ је уклоњен"
},
"autofillSuggestions": {
"message": "Предлози за ауто-попуњавање"
@ -4358,7 +4358,7 @@
"message": "Подаци"
},
"passkeys": {
"message": "Приступачни кључеви",
"message": "Приступни кључеви",
"description": "A section header for a list of passkeys."
},
"passwords": {
@ -4366,7 +4366,7 @@
"description": "A section header for a list of passwords."
},
"logInWithPasskeyAriaLabel": {
"message": "Пријавите се са приступачним кључем",
"message": "Пријавите се са приступним кључем",
"description": "ARIA label for the inline menu button that logs in with a passkey."
},
"assign": {
@ -4564,13 +4564,13 @@
"message": "Смештај ставке"
},
"fileSend": {
"message": "File Send"
"message": "Датотека „Send“"
},
"fileSends": {
"message": "Датотека „Send“"
},
"textSend": {
"message": "Text Send"
"message": "Текст „Send“"
},
"textSends": {
"message": "Текст „Send“"
@ -4603,7 +4603,7 @@
"message": "Додатни садржај је доступан"
},
"fileSavedToDevice": {
"message": "File saved to device. Manage from your device downloads."
"message": "Датотека је сачувана на уређају. Управљајте преузимањима са свог уређаја."
},
"showCharacterCount": {
"message": "Прикажи бројање слова"
@ -4636,23 +4636,23 @@
"message": "Аутентификација"
},
"fillGeneratedPassword": {
"message": "Fill generated password",
"message": "Попуните генерисану лозинку",
"description": "Heading for the password generator within the inline menu"
},
"passwordRegenerated": {
"message": "Password regenerated",
"message": "Лозинка поново генерисана",
"description": "Notification message for when a password has been regenerated"
},
"saveLoginToBitwarden": {
"message": "Save login to Bitwarden?",
"message": "Сачувати пријаву на Bitwarden?",
"description": "Confirmation message for saving a login to Bitwarden"
},
"spaceCharacterDescriptor": {
"message": "Space",
"message": "Простор",
"description": "Represents the space key in screen reader content as a readable word"
},
"tildeCharacterDescriptor": {
"message": "Tilde",
"message": "Тилда",
"description": "Represents the ~ key in screen reader content as a readable word"
},
"backtickCharacterDescriptor": {
@ -4660,55 +4660,55 @@
"description": "Represents the ` key in screen reader content as a readable word"
},
"exclamationCharacterDescriptor": {
"message": "Exclamation mark",
"message": "Узвичник",
"description": "Represents the ! key in screen reader content as a readable word"
},
"atSignCharacterDescriptor": {
"message": "At sign",
"message": "Знак „ет“",
"description": "Represents the @ key in screen reader content as a readable word"
},
"hashSignCharacterDescriptor": {
"message": "Hash sign",
"message": "Знак „хеш“",
"description": "Represents the # key in screen reader content as a readable word"
},
"dollarSignCharacterDescriptor": {
"message": "Dollar sign",
"message": "Знак долар",
"description": "Represents the $ key in screen reader content as a readable word"
},
"percentSignCharacterDescriptor": {
"message": "Percent sign",
"message": "Знак постотак",
"description": "Represents the % key in screen reader content as a readable word"
},
"caretCharacterDescriptor": {
"message": "Caret",
"message": "Знак за уметање",
"description": "Represents the ^ key in screen reader content as a readable word"
},
"ampersandCharacterDescriptor": {
"message": "Ampersand",
"message": "Знак Ampersand",
"description": "Represents the & key in screen reader content as a readable word"
},
"asteriskCharacterDescriptor": {
"message": "Asterisk",
"message": "Знак звездица",
"description": "Represents the * key in screen reader content as a readable word"
},
"parenLeftCharacterDescriptor": {
"message": "Left parenthesis",
"message": "Отворена заграда",
"description": "Represents the ( key in screen reader content as a readable word"
},
"parenRightCharacterDescriptor": {
"message": "Right parenthesis",
"message": "Затворена заграда",
"description": "Represents the ) key in screen reader content as a readable word"
},
"hyphenCharacterDescriptor": {
"message": "Underscore",
"message": "Доња црта",
"description": "Represents the _ key in screen reader content as a readable word"
},
"underscoreCharacterDescriptor": {
"message": "Hyphen",
"message": "Цртица",
"description": "Represents the - key in screen reader content as a readable word"
},
"plusCharacterDescriptor": {
"message": "Plus",
"message": "Плус",
"description": "Represents the + key in screen reader content as a readable word"
},
"equalsCharacterDescriptor": {

View File

@ -1511,7 +1511,7 @@
"message": "Komprometterade eller ej betrodda webbplatser kan utnyttja automatisk ifyllnad vid sidladdning."
},
"learnMoreAboutAutofillOnPageLoadLinkText": {
"message": "Learn more about risks"
"message": "Läs mer om risker"
},
"learnMoreAboutAutofill": {
"message": "Läs mer om automatisk ifyllnad"
@ -2411,7 +2411,7 @@
"message": "Lösenordsskyddad"
},
"copyLink": {
"message": "Copy link"
"message": "Kopiera länk"
},
"copySendLink": {
"message": "Kopiera Send-länk",

View File

@ -2878,7 +2878,7 @@
"message": "Генерувати е-пошту"
},
"generatorBoundariesHint": {
"message": "Value must be between $MIN$ and $MAX$",
"message": "Значення має бути між $MIN$ та $MAX$",
"description": "Explains spin box minimum and maximum values to the user",
"placeholders": {
"min": {

View File

@ -20,16 +20,16 @@
"message": "创建账户"
},
"newToBitwarden": {
"message": "New to Bitwarden?"
"message": "Bitwarden 新手吗?"
},
"logInWithPasskey": {
"message": "Log in with passkey"
"message": "使用通行密钥登录"
},
"useSingleSignOn": {
"message": "Use single sign-on"
"message": "使用单点登录"
},
"welcomeBack": {
"message": "Welcome back"
"message": "欢迎回来"
},
"setAStrongPassword": {
"message": "设置强密码"
@ -120,7 +120,7 @@
"message": "复制密码"
},
"copyPassphrase": {
"message": "Copy passphrase"
"message": "复制密码短语"
},
"copyNote": {
"message": "复制备注"
@ -168,7 +168,7 @@
"message": "复制备注"
},
"fill": {
"message": "Fill",
"message": "填充",
"description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible."
},
"autoFill": {
@ -427,7 +427,7 @@
"message": "生成密码"
},
"generatePassphrase": {
"message": "Generate passphrase"
"message": "生成密码短语"
},
"regeneratePassword": {
"message": "重新生成密码"
@ -591,7 +591,7 @@
"message": "启动网站"
},
"launchWebsiteName": {
"message": "Launch website $ITEMNAME$",
"message": "前往 $ITEMNAME$ 的网站",
"placeholders": {
"itemname": {
"content": "$1",
@ -846,7 +846,7 @@
"message": "登录"
},
"logInToBitwarden": {
"message": "Log in to Bitwarden"
"message": "登录到 Bitwarden"
},
"restartRegistration": {
"message": "重新开始注册"
@ -1424,7 +1424,7 @@
"message": "服务器 URL"
},
"selfHostBaseUrl": {
"message": "Self-host server URL",
"message": "自托管服务器 URL",
"description": "Label for field requesting a self-hosted integration service URL"
},
"apiUrl": {
@ -1795,13 +1795,13 @@
"message": "密码历史记录"
},
"generatorHistory": {
"message": "Generator history"
"message": "生成器历史记录"
},
"clearGeneratorHistoryTitle": {
"message": "Clear generator history"
"message": "清除生成器历史记录"
},
"cleargGeneratorHistoryDescription": {
"message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?"
"message": "若继续,所有条目将从生成器历史记录中永久删除。确定要继续吗?"
},
"back": {
"message": "后退"
@ -1920,10 +1920,10 @@
"message": "清除历史记录"
},
"nothingToShow": {
"message": "Nothing to show"
"message": "没有可显示的内容"
},
"nothingGeneratedRecently": {
"message": "You haven't generated anything recently"
"message": "您最近没有生成任何内容"
},
"remove": {
"message": "移除"
@ -2512,7 +2512,7 @@
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendPasswordDescV3": {
"message": "Add an optional password for recipients to access this Send.",
"message": "添加一个用于收件人访问此 Send 的可选密码。",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendNotesDesc": {
@ -2875,10 +2875,10 @@
"message": "生成用户名"
},
"generateEmail": {
"message": "Generate email"
"message": "生成邮件地址"
},
"generatorBoundariesHint": {
"message": "Value must be between $MIN$ and $MAX$",
"message": "值必须在 $MIN$ 和 $MAX$ 之间",
"description": "Explains spin box minimum and maximum values to the user",
"placeholders": {
"min": {
@ -2932,11 +2932,11 @@
"message": "使用外部转发服务生成一个电子邮件别名。"
},
"forwarderDomainName": {
"message": "Email domain",
"message": "邮件域名",
"description": "Labels the domain name email forwarder service option"
},
"forwarderDomainNameHint": {
"message": "Choose a domain that is supported by the selected service",
"message": "选择一个所选服务支持的域名",
"description": "Guidance provided for email forwarding services that support multiple email domains."
},
"forwarderError": {
@ -4636,156 +4636,156 @@
"message": "正在验证"
},
"fillGeneratedPassword": {
"message": "Fill generated password",
"message": "填充已生成的密码",
"description": "Heading for the password generator within the inline menu"
},
"passwordRegenerated": {
"message": "Password regenerated",
"message": "密码已重新生成",
"description": "Notification message for when a password has been regenerated"
},
"saveLoginToBitwarden": {
"message": "Save login to Bitwarden?",
"message": "将登录保存到 Bitwarden 吗?",
"description": "Confirmation message for saving a login to Bitwarden"
},
"spaceCharacterDescriptor": {
"message": "Space",
"message": "空格",
"description": "Represents the space key in screen reader content as a readable word"
},
"tildeCharacterDescriptor": {
"message": "Tilde",
"message": "波浪号",
"description": "Represents the ~ key in screen reader content as a readable word"
},
"backtickCharacterDescriptor": {
"message": "Backtick",
"message": "反引号",
"description": "Represents the ` key in screen reader content as a readable word"
},
"exclamationCharacterDescriptor": {
"message": "Exclamation mark",
"message": "感叹号",
"description": "Represents the ! key in screen reader content as a readable word"
},
"atSignCharacterDescriptor": {
"message": "At sign",
"message": "艾特号",
"description": "Represents the @ key in screen reader content as a readable word"
},
"hashSignCharacterDescriptor": {
"message": "Hash sign",
"message": "井号",
"description": "Represents the # key in screen reader content as a readable word"
},
"dollarSignCharacterDescriptor": {
"message": "Dollar sign",
"message": "美元符号",
"description": "Represents the $ key in screen reader content as a readable word"
},
"percentSignCharacterDescriptor": {
"message": "Percent sign",
"message": "百分号",
"description": "Represents the % key in screen reader content as a readable word"
},
"caretCharacterDescriptor": {
"message": "Caret",
"message": "脱字符",
"description": "Represents the ^ key in screen reader content as a readable word"
},
"ampersandCharacterDescriptor": {
"message": "Ampersand",
"message": "与和符",
"description": "Represents the & key in screen reader content as a readable word"
},
"asteriskCharacterDescriptor": {
"message": "Asterisk",
"message": "星号",
"description": "Represents the * key in screen reader content as a readable word"
},
"parenLeftCharacterDescriptor": {
"message": "Left parenthesis",
"message": "左括号",
"description": "Represents the ( key in screen reader content as a readable word"
},
"parenRightCharacterDescriptor": {
"message": "Right parenthesis",
"message": "右括号",
"description": "Represents the ) key in screen reader content as a readable word"
},
"hyphenCharacterDescriptor": {
"message": "Underscore",
"message": "下划线",
"description": "Represents the _ key in screen reader content as a readable word"
},
"underscoreCharacterDescriptor": {
"message": "Hyphen",
"message": "连字符",
"description": "Represents the - key in screen reader content as a readable word"
},
"plusCharacterDescriptor": {
"message": "Plus",
"message": "加号",
"description": "Represents the + key in screen reader content as a readable word"
},
"equalsCharacterDescriptor": {
"message": "Equals",
"message": "等号",
"description": "Represents the = key in screen reader content as a readable word"
},
"braceLeftCharacterDescriptor": {
"message": "Left brace",
"message": "左大括号",
"description": "Represents the { key in screen reader content as a readable word"
},
"braceRightCharacterDescriptor": {
"message": "Right brace",
"message": "右大括号",
"description": "Represents the } key in screen reader content as a readable word"
},
"bracketLeftCharacterDescriptor": {
"message": "Left bracket",
"message": "左中括号",
"description": "Represents the [ key in screen reader content as a readable word"
},
"bracketRightCharacterDescriptor": {
"message": "Right bracket",
"message": "右中括号",
"description": "Represents the ] key in screen reader content as a readable word"
},
"pipeCharacterDescriptor": {
"message": "Pipe",
"message": "竖线",
"description": "Represents the | key in screen reader content as a readable word"
},
"backSlashCharacterDescriptor": {
"message": "Back slash",
"message": "反斜杠",
"description": "Represents the back slash key in screen reader content as a readable word"
},
"colonCharacterDescriptor": {
"message": "Colon",
"message": "冒号",
"description": "Represents the : key in screen reader content as a readable word"
},
"semicolonCharacterDescriptor": {
"message": "Semicolon",
"message": "分号",
"description": "Represents the ; key in screen reader content as a readable word"
},
"doubleQuoteCharacterDescriptor": {
"message": "Double quote",
"message": "双引号",
"description": "Represents the double quote key in screen reader content as a readable word"
},
"singleQuoteCharacterDescriptor": {
"message": "Single quote",
"message": "单引号",
"description": "Represents the ' key in screen reader content as a readable word"
},
"lessThanCharacterDescriptor": {
"message": "Less than",
"message": "小于号",
"description": "Represents the < key in screen reader content as a readable word"
},
"greaterThanCharacterDescriptor": {
"message": "Greater than",
"message": "大于号",
"description": "Represents the > key in screen reader content as a readable word"
},
"commaCharacterDescriptor": {
"message": "Comma",
"message": "逗号",
"description": "Represents the , key in screen reader content as a readable word"
},
"periodCharacterDescriptor": {
"message": "Period",
"message": "句号",
"description": "Represents the . key in screen reader content as a readable word"
},
"questionCharacterDescriptor": {
"message": "Question mark",
"message": "问号",
"description": "Represents the ? key in screen reader content as a readable word"
},
"forwardSlashCharacterDescriptor": {
"message": "Forward slash",
"message": "正斜杠",
"description": "Represents the / key in screen reader content as a readable word"
},
"lowercaseAriaLabel": {
"message": "Lowercase"
"message": "小写"
},
"uppercaseAriaLabel": {
"message": "Uppercase"
"message": "大写"
},
"generatedPassword": {
"message": "Generated password"
"message": "生成密码"
}
}

View File

@ -9,7 +9,7 @@
<ng-container slot="end">
<app-pop-out></app-pop-out>
<app-current-account *ngIf="showAcctSwitcher"></app-current-account>
<app-current-account *ngIf="showAcctSwitcher && hasLoggedInAccount"></app-current-account>
</ng-container>
</popup-header>

View File

@ -15,6 +15,7 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
import { CurrentAccountComponent } from "../account-switching/current-account.component";
import { AccountSwitcherService } from "../account-switching/services/account-switcher.service";
import { ExtensionBitwardenLogo } from "./extension-bitwarden-logo.icon";
@ -50,6 +51,7 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy {
protected pageIcon: Icon;
protected showReadonlyHostname: boolean;
protected maxWidth: "md" | "3xl";
protected hasLoggedInAccount: boolean = false;
protected theme: string;
protected logo = ExtensionBitwardenLogo;
@ -59,6 +61,7 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy {
private route: ActivatedRoute,
private i18nService: I18nService,
private extensionAnonLayoutWrapperDataService: AnonLayoutWrapperDataService,
private accountSwitcherService: AccountSwitcherService,
) {}
async ngOnInit(): Promise<void> {
@ -68,6 +71,12 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy {
// Listen for page changes and update the page data appropriately
this.listenForPageDataChanges();
this.listenForServiceDataChanges();
this.accountSwitcherService.availableAccounts$
.pipe(takeUntil(this.destroy$))
.subscribe((accounts) => {
this.hasLoggedInAccount = accounts.some((account) => account.id !== "addAccount");
});
}
private listenForPageDataChanges() {

View File

@ -27,6 +27,7 @@ import { ButtonModule, I18nMockService } from "@bitwarden/components";
import { RegistrationCheckEmailIcon } from "../../../../../../libs/auth/src/angular/icons/registration-check-email.icon";
import { PopupRouterCacheService } from "../../../platform/popup/view-cache/popup-router-cache.service";
import { AccountSwitcherService } from "../account-switching/services/account-switcher.service";
import { ExtensionAnonLayoutWrapperDataService } from "./extension-anon-layout-wrapper-data.service";
import {
@ -45,6 +46,7 @@ const decorators = (options: {
applicationVersion?: string;
clientType?: ClientType;
hostName?: string;
accounts?: any[];
}) => {
return [
componentWrapperDecorator(
@ -83,6 +85,13 @@ const decorators = (options: {
}),
},
},
{
provide: AccountSwitcherService,
useValue: {
availableAccounts$: of(options.accounts || []),
SPECIAL_ADD_ACCOUNT_ID: "addAccount",
} as Partial<AccountSwitcherService>,
},
{
provide: AuthService,
useValue: {
@ -300,3 +309,64 @@ export const DynamicContentExample: Story = {
],
}),
};
export const HasLoggedInAccountExample: Story = {
render: (args) => ({
props: args,
template: "<router-outlet></router-outlet>",
}),
decorators: decorators({
components: [DefaultPrimaryOutletExampleComponent],
routes: [
{
path: "**",
redirectTo: "has-logged-in-account",
pathMatch: "full",
},
{
path: "",
component: ExtensionAnonLayoutWrapperComponent,
children: [
{
path: "has-logged-in-account",
data: {
hasLoggedInAccount: true,
showAcctSwitcher: true,
},
children: [
{
path: "",
component: DefaultPrimaryOutletExampleComponent,
},
{
path: "",
component: DefaultSecondaryOutletExampleComponent,
outlet: "secondary",
},
{
path: "",
component: DefaultEnvSelectorOutletExampleComponent,
outlet: "environment-selector",
},
],
},
],
},
],
accounts: [
{
name: "Test User",
email: "testuser@bitwarden.com",
id: "123e4567-e89b-12d3-a456-426614174000",
server: "bitwarden.com",
status: 2,
isActive: false,
},
{
name: "addAccount",
id: "addAccount",
isActive: false,
},
],
}),
};

View File

@ -1,10 +1,12 @@
import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { Router } from "@angular/router";
import { Subject, firstValueFrom, switchMap, takeUntil } from "rxjs";
import { ActivatedRoute, Router } from "@angular/router";
import { Subject, firstValueFrom, switchMap, takeUntil, tap } from "rxjs";
import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component";
import { LoginEmailServiceAbstraction, RegisterRouteService } from "@bitwarden/auth/common";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ToastService } from "@bitwarden/components";
@ -38,9 +40,13 @@ export class HomeComponent implements OnInit, OnDestroy {
private accountSwitcherService: AccountSwitcherService,
private registerRouteService: RegisterRouteService,
private toastService: ToastService,
private configService: ConfigService,
private route: ActivatedRoute,
) {}
async ngOnInit(): Promise<void> {
this.listenForUnauthUiRefreshFlagChanges();
const email = await firstValueFrom(this.loginEmailService.loginEmail$);
const rememberEmail = this.loginEmailService.getRememberEmail();
@ -70,6 +76,29 @@ export class HomeComponent implements OnInit, OnDestroy {
this.destroyed$.complete();
}
private listenForUnauthUiRefreshFlagChanges() {
this.configService
.getFeatureFlag$(FeatureFlag.UnauthenticatedExtensionUIRefresh)
.pipe(
tap(async (flag) => {
// If the flag is turned ON, we must force a reload to ensure the correct UI is shown
if (flag) {
const uniqueQueryParams = {
...this.route.queryParams,
// adding a unique timestamp to the query params to force a reload
t: new Date().getTime().toString(),
};
await this.router.navigate(["/login"], {
queryParams: uniqueQueryParams,
});
}
}),
takeUntil(this.destroyed$),
)
.subscribe();
}
get availableAccounts$() {
return this.accountSwitcherService.availableAccounts$;
}

View File

@ -216,7 +216,6 @@ export type OverlayBackgroundExtensionMessageHandlers = {
getCurrentTabFrameId: ({ sender }: BackgroundSenderParam) => number;
updateSubFrameData: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
triggerSubFrameFocusInRebuild: ({ sender }: BackgroundSenderParam) => void;
shouldRepositionSubFrameInlineMenuOnScroll: ({ sender }: BackgroundSenderParam) => void;
destroyAutofillInlineMenuListeners: ({
message,
sender,

View File

@ -32,6 +32,7 @@ import {
} from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@ -106,6 +107,7 @@ describe("OverlayBackground", () => {
let selectedThemeMock$: BehaviorSubject<ThemeType>;
let inlineMenuFieldQualificationService: InlineMenuFieldQualificationService;
let themeStateService: MockProxy<ThemeStateService>;
let totpService: MockProxy<TotpService>;
let overlayBackground: OverlayBackground;
let portKeyForTabSpy: Record<number, string>;
let pageDetailsForTabSpy: PageDetailsForTab;
@ -184,6 +186,7 @@ describe("OverlayBackground", () => {
inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();
themeStateService = mock<ThemeStateService>();
themeStateService.selectedTheme$ = selectedThemeMock$;
totpService = mock<TotpService>();
overlayBackground = new OverlayBackground(
logService,
cipherService,
@ -198,6 +201,7 @@ describe("OverlayBackground", () => {
fido2ActiveRequestManager,
inlineMenuFieldQualificationService,
themeStateService,
totpService,
generatedPasswordCallbackMock,
addPasswordCallbackMock,
);
@ -629,9 +633,7 @@ describe("OverlayBackground", () => {
it("skips updating the inline menu list if the user has the inline menu set to open on button click", async () => {
inlineMenuVisibilityMock$.next(AutofillOverlayVisibility.OnButtonClick);
jest
.spyOn(overlayBackground as any, "checkIsInlineMenuListVisible")
.mockReturnValue(false);
overlayBackground["inlineMenuListPort"] = null;
tabsSendMessageSpy.mockImplementation((_tab, message, _options) => {
if (message.command === "checkFocusedFieldHasValue") {
return Promise.resolve(true);
@ -2267,7 +2269,7 @@ describe("OverlayBackground", () => {
});
it("closes the list if the user has the inline menu set to show on button click and the list is open", async () => {
overlayBackground["isInlineMenuListVisible"] = true;
overlayBackground["inlineMenuListPort"] = listPortSpy;
inlineMenuVisibilityMock$.next(AutofillOverlayVisibility.OnButtonClick);
sendMockExtensionMessage({ command: "openAutofillInlineMenu" }, sender);

View File

@ -33,6 +33,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { buildCipherIcon } from "@bitwarden/common/vault/icon/build-cipher-icon";
@ -168,8 +169,6 @@ export class OverlayBackground implements OverlayBackgroundInterface {
getCurrentTabFrameId: ({ sender }) => this.getSenderFrameId(sender),
updateSubFrameData: ({ message, sender }) => this.updateSubFrameData(message, sender),
triggerSubFrameFocusInRebuild: ({ sender }) => this.triggerSubFrameFocusInRebuild(sender),
shouldRepositionSubFrameInlineMenuOnScroll: ({ sender }) =>
this.shouldRepositionSubFrameInlineMenuOnScroll(sender),
destroyAutofillInlineMenuListeners: ({ message, sender }) =>
this.triggerDestroyInlineMenuListeners(sender.tab, message.subFrameData.frameId),
collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender),
@ -219,6 +218,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
private fido2ActiveRequestManager: Fido2ActiveRequestManager,
private inlineMenuFieldQualificationService: InlineMenuFieldQualificationService,
private themeStateService: ThemeStateService,
private totpService: TotpService,
private generatePasswordCallback: () => Promise<string>,
private addPasswordCallback: (password: string) => Promise<void>,
) {
@ -1010,7 +1010,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
);
if (
!this.checkIsInlineMenuListVisible() &&
!this.inlineMenuListPort &&
(await this.getInlineMenuVisibility()) === AutofillOverlayVisibility.OnButtonClick
) {
return;
@ -1060,7 +1060,6 @@ export class OverlayBackground implements OverlayBackgroundInterface {
}
const cipher = this.inlineMenuCiphers.get(inlineMenuCipherId);
if (usePasskey && cipher.login?.hasFido2Credentials) {
await this.authenticatePasskeyCredential(
sender,
@ -1068,6 +1067,11 @@ export class OverlayBackground implements OverlayBackgroundInterface {
);
this.updateLastUsedInlineMenuCipher(inlineMenuCipherId, cipher);
if (cipher.login?.totp) {
this.platformUtilsService.copyToClipboard(
await this.totpService.getCode(cipher.login.totp),
);
}
return;
}
@ -1819,7 +1823,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
return;
}
if (this.isInlineMenuListVisible) {
if (this.inlineMenuListPort) {
this.closeInlineMenu(sender, {
forceCloseInlineMenu: true,
overlayElement: AutofillOverlayElement.List,
@ -2600,20 +2604,6 @@ export class OverlayBackground implements OverlayBackgroundInterface {
this.repositionInlineMenu$.next(sender);
}
/**
* Triggers on scroll of a frame within the tab. Will reposition the inline menu
* if the focused field is within a sub-frame and the inline menu is visible.
*
* @param sender - The sender of the message
*/
private shouldRepositionSubFrameInlineMenuOnScroll(sender: chrome.runtime.MessageSender) {
if (!this.isInlineMenuButtonVisible || sender.tab.id !== this.focusedFieldData?.tabId) {
return false;
}
return this.focusedFieldData.frameId > 0;
}
/**
* Handles determining if the inline menu should be repositioned or closed, and initiates
* the process of calculating the new position of the inline menu.

View File

@ -219,7 +219,11 @@ export class AutofillComponent implements OnInit {
: AutofillOverlayVisibility.Off;
await this.autofillSettingsService.setInlineMenuVisibility(newInlineMenuVisibilityValue);
await this.requestPrivacyPermission();
// No need to initiate browser permission request if a feature is being turned off
if (newInlineMenuVisibilityValue !== AutofillOverlayVisibility.Off) {
await this.requestPrivacyPermission();
}
}
async updateAutofillOnPageLoad() {

View File

@ -104,6 +104,7 @@ export class CreditCardAutoFillConstants {
];
static readonly CardHolderFieldNames: string[] = [
"accountholdername",
"cc-name",
"card-name",
"cardholder-name",
@ -113,6 +114,7 @@ export class CreditCardAutoFillConstants {
];
static readonly CardHolderFieldNameValues: string[] = [
"accountholdername",
"cc-name",
"card-name",
"cardholder-name",

View File

@ -1703,6 +1703,10 @@ describe("AutofillOverlayContentService", () => {
const repositionEvents = [EVENTS.SCROLL, EVENTS.RESIZE];
repositionEvents.forEach((repositionEvent) => {
it(`sends a message trigger overlay reposition message to the background when a ${repositionEvent} event occurs`, async () => {
Object.defineProperty(globalThis, "scrollY", {
value: 10,
writable: true,
});
sendExtensionMessageSpy.mockResolvedValueOnce(true);
globalThis.dispatchEvent(new Event(repositionEvent));
await flushPromises();

View File

@ -1568,41 +1568,46 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
* the overlay elements on scroll or resize.
*/
private setOverlayRepositionEventListeners() {
let currentScrollY = globalThis.scrollY;
let currentScrollX = globalThis.scrollX;
let mostRecentTargetScrollY = 0;
const repositionHandler = this.useEventHandlersMemo(
throttle(this.handleOverlayRepositionEvent, 250),
AUTOFILL_OVERLAY_HANDLE_REPOSITION,
);
const eventTargetContainsFocusedField = (eventTarget: Element | Document) => {
if (!eventTarget || !this.mostRecentlyFocusedField) {
return false;
}
const activeElement = (eventTarget as Document).activeElement;
if (activeElement) {
return (
activeElement === this.mostRecentlyFocusedField ||
activeElement.contains(this.mostRecentlyFocusedField) ||
this.inlineMenuContentService?.isElementInlineMenu(activeElement as HTMLElement)
);
}
const eventTargetContainsFocusedField = (eventTarget: Element) => {
if (typeof eventTarget.contains !== "function") {
return false;
}
return (
const targetScrollY = eventTarget.scrollTop;
if (targetScrollY === mostRecentTargetScrollY) {
return false;
}
if (
eventTarget === this.mostRecentlyFocusedField ||
eventTarget.contains(this.mostRecentlyFocusedField)
);
) {
mostRecentTargetScrollY = targetScrollY;
return true;
}
return false;
};
const scrollHandler = this.useEventHandlersMemo(
throttle(async (event) => {
if (
eventTargetContainsFocusedField(event.target) ||
(await this.shouldRepositionSubFrameInlineMenuOnScroll())
currentScrollY !== globalThis.scrollY ||
currentScrollX !== globalThis.scrollX ||
eventTargetContainsFocusedField(event.target)
) {
repositionHandler(event);
}
currentScrollY = globalThis.scrollY;
currentScrollX = globalThis.scrollX;
}, 50),
AUTOFILL_OVERLAY_HANDLE_SCROLL,
);

View File

@ -980,7 +980,7 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
const queueLength = this.mutationsQueue.length;
if (!this.domQueryService.pageContainsShadowDomElements()) {
this.domQueryService.checkPageContainsShadowDom();
this.checkPageContainsShadowDom();
}
for (let queueIndex = 0; queueIndex < queueLength; queueIndex++) {
@ -999,6 +999,29 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
this.mutationsQueue = [];
};
/**
* Handles checking if the current page contains a ShadowDOM element and
* flags that a re-collection of page details is required if it does.
*/
private checkPageContainsShadowDom() {
this.domQueryService.checkPageContainsShadowDom();
if (this.domQueryService.pageContainsShadowDomElements()) {
this.flagPageDetailsUpdateIsRequired();
}
}
/**
* Triggers several flags that indicate that a collection of page details should
* occur again on a subsequent call after a mutation has been observed in the DOM.
*/
private flagPageDetailsUpdateIsRequired() {
this.domRecentlyMutated = true;
if (this.autofillOverlayContentService) {
this.autofillOverlayContentService.pageDetailsUpdateRequired = true;
}
this.noFieldsFound = false;
}
/**
* Processes all mutation records encountered by the mutation observer.
*
@ -1023,11 +1046,7 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
(this.isAutofillElementNodeMutated(mutation.removedNodes, true) ||
this.isAutofillElementNodeMutated(mutation.addedNodes))
) {
this.domRecentlyMutated = true;
if (this.autofillOverlayContentService) {
this.autofillOverlayContentService.pageDetailsUpdateRequired = true;
}
this.noFieldsFound = false;
this.flagPageDetailsUpdateIsRequired();
return;
}

View File

@ -257,12 +257,9 @@ import { BrowserPlatformUtilsService } from "../platform/services/platform-utils
import { PopupViewCacheBackgroundService } from "../platform/services/popup-view-cache-background.service";
import { BrowserSdkClientFactory } from "../platform/services/sdk/browser-sdk-client-factory";
import { BackgroundTaskSchedulerService } from "../platform/services/task-scheduler/background-task-scheduler.service";
import { ForegroundTaskSchedulerService } from "../platform/services/task-scheduler/foreground-task-scheduler.service";
import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service";
import { BrowserStorageServiceProvider } from "../platform/storage/browser-storage-service.provider";
import { ForegroundMemoryStorageService } from "../platform/storage/foreground-memory-storage.service";
import { OffscreenStorageService } from "../platform/storage/offscreen-storage.service";
import { ForegroundSyncService } from "../platform/sync/foreground-sync.service";
import { SyncServiceListener } from "../platform/sync/sync-service.listener";
import { fromChromeRuntimeMessaging } from "../platform/utils/from-chrome-runtime-messaging";
import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service";
@ -401,7 +398,7 @@ export default class MainBackground {
private popupViewCacheBackgroundService: PopupViewCacheBackgroundService;
constructor(public popupOnlyContext: boolean = false) {
constructor() {
// Services
const lockedCallback = async (userId?: string) => {
if (this.notificationsService != null) {
@ -460,45 +457,6 @@ export default class MainBackground {
this.offscreenDocumentService,
);
// Creates a session key for mv3 storage of large memory items
const sessionKey = new Lazy(async () => {
// Key already in session storage
const sessionStorage = new BrowserMemoryStorageService();
const existingKey = await sessionStorage.get<SymmetricCryptoKey>("session-key");
if (existingKey) {
if (sessionStorage.valuesRequireDeserialization) {
return SymmetricCryptoKey.fromJSON(existingKey);
}
return existingKey;
}
// New key
const { derivedKey } = await this.keyGenerationService.createKeyWithPurpose(
128,
"ephemeral",
"bitwarden-ephemeral",
);
await sessionStorage.save("session-key", derivedKey);
return derivedKey;
});
const mv3MemoryStorageCreator = () => {
if (this.popupOnlyContext) {
return new ForegroundMemoryStorageService();
}
// For local backed session storage, we expect that the encrypted data on disk will persist longer than the encryption key in memory
// and failures to decrypt because of that are completely expected. For this reason, we pass in `false` to the `EncryptServiceImplementation`
// so that MAC failures are not logged.
return new LocalBackedSessionStorageService(
sessionKey,
this.storageService,
new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false),
this.platformUtilsService,
this.logService,
);
};
this.secureStorageService = this.storageService; // secure storage is not supported in browsers, so we use local storage and warn users when it is used
if (BrowserApi.isManifestVersion(3)) {
@ -506,18 +464,47 @@ export default class MainBackground {
this.memoryStorageForStateProviders = new BrowserMemoryStorageService(); // mv3 stores to storage.session
this.memoryStorageService = this.memoryStorageForStateProviders;
} else {
if (popupOnlyContext) {
this.memoryStorageForStateProviders = new ForegroundMemoryStorageService();
this.memoryStorageService = new ForegroundMemoryStorageService();
} else {
this.memoryStorageForStateProviders = new BackgroundMemoryStorageService(); // mv2 stores to memory
this.memoryStorageService = this.memoryStorageForStateProviders;
}
this.memoryStorageForStateProviders = new BackgroundMemoryStorageService(); // mv2 stores to memory
this.memoryStorageService = this.memoryStorageForStateProviders;
}
this.largeObjectMemoryStorageForStateProviders = BrowserApi.isManifestVersion(3)
? mv3MemoryStorageCreator() // mv3 stores to local-backed session storage
: this.memoryStorageForStateProviders; // mv2 stores to the same location
if (BrowserApi.isManifestVersion(3)) {
// Creates a session key for mv3 storage of large memory items
const sessionKey = new Lazy(async () => {
// Key already in session storage
const sessionStorage = new BrowserMemoryStorageService();
const existingKey = await sessionStorage.get<SymmetricCryptoKey>("session-key");
if (existingKey) {
if (sessionStorage.valuesRequireDeserialization) {
return SymmetricCryptoKey.fromJSON(existingKey);
}
return existingKey;
}
// New key
const { derivedKey } = await this.keyGenerationService.createKeyWithPurpose(
128,
"ephemeral",
"bitwarden-ephemeral",
);
await sessionStorage.save("session-key", derivedKey);
return derivedKey;
});
this.largeObjectMemoryStorageForStateProviders = new LocalBackedSessionStorageService(
sessionKey,
this.storageService,
// For local backed session storage, we expect that the encrypted data on disk will persist longer than the encryption key in memory
// and failures to decrypt because of that are completely expected. For this reason, we pass in `false` to the `EncryptServiceImplementation`
// so that MAC failures are not logged.
new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false),
this.platformUtilsService,
this.logService,
);
} else {
// mv2 stores to the same location
this.largeObjectMemoryStorageForStateProviders = this.memoryStorageForStateProviders;
}
const localStorageStorageService = BrowserApi.isManifestVersion(3)
? new OffscreenStorageService(this.offscreenDocumentService)
@ -575,9 +562,10 @@ export default class MainBackground {
this.derivedStateProvider,
);
this.taskSchedulerService = this.popupOnlyContext
? new ForegroundTaskSchedulerService(this.logService, this.stateProvider)
: new BackgroundTaskSchedulerService(this.logService, this.stateProvider);
this.taskSchedulerService = new BackgroundTaskSchedulerService(
this.logService,
this.stateProvider,
);
this.taskSchedulerService.registerTaskHandler(ScheduledTaskNames.scheduleNextSyncInterval, () =>
this.fullSync(),
);
@ -632,6 +620,7 @@ export default class MainBackground {
this.stateService,
this.keyGenerationService,
this.encryptService,
this.logService,
);
this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider);
@ -872,26 +861,24 @@ export default class MainBackground {
this.vaultSettingsService = new VaultSettingsService(this.stateProvider);
if (!this.popupOnlyContext) {
this.vaultTimeoutService = new VaultTimeoutService(
this.accountService,
this.masterPasswordService,
this.cipherService,
this.folderService,
this.collectionService,
this.platformUtilsService,
this.messagingService,
this.searchService,
this.stateService,
this.authService,
this.vaultTimeoutSettingsService,
this.stateEventRunnerService,
this.taskSchedulerService,
this.logService,
lockedCallback,
logoutCallback,
);
}
this.vaultTimeoutService = new VaultTimeoutService(
this.accountService,
this.masterPasswordService,
this.cipherService,
this.folderService,
this.collectionService,
this.platformUtilsService,
this.messagingService,
this.searchService,
this.stateService,
this.authService,
this.vaultTimeoutSettingsService,
this.stateEventRunnerService,
this.taskSchedulerService,
this.logService,
lockedCallback,
logoutCallback,
);
this.containerService = new ContainerService(this.keyService, this.encryptService);
this.sendStateProvider = new SendStateProvider(this.stateProvider);
@ -912,59 +899,41 @@ export default class MainBackground {
this.providerService = new ProviderService(this.stateProvider);
if (this.popupOnlyContext) {
this.syncService = new ForegroundSyncService(
this.stateService,
this.folderService,
this.folderApiService,
this.messagingService,
this.logService,
this.cipherService,
this.collectionService,
this.apiService,
this.accountService,
this.authService,
this.sendService,
this.sendApiService,
messageListener,
this.stateProvider,
);
} else {
this.syncService = new DefaultSyncService(
this.masterPasswordService,
this.accountService,
this.apiService,
this.domainSettingsService,
this.folderService,
this.cipherService,
this.keyService,
this.collectionService,
this.messagingService,
this.policyService,
this.sendService,
this.logService,
this.keyConnectorService,
this.stateService,
this.providerService,
this.folderApiService,
this.organizationService,
this.sendApiService,
this.userDecryptionOptionsService,
this.avatarService,
logoutCallback,
this.billingAccountProfileStateService,
this.tokenService,
this.authService,
this.stateProvider,
);
this.syncService = new DefaultSyncService(
this.masterPasswordService,
this.accountService,
this.apiService,
this.domainSettingsService,
this.folderService,
this.cipherService,
this.keyService,
this.collectionService,
this.messagingService,
this.policyService,
this.sendService,
this.logService,
this.keyConnectorService,
this.stateService,
this.providerService,
this.folderApiService,
this.organizationService,
this.sendApiService,
this.userDecryptionOptionsService,
this.avatarService,
logoutCallback,
this.billingAccountProfileStateService,
this.tokenService,
this.authService,
this.stateProvider,
);
this.syncServiceListener = new SyncServiceListener(
this.syncService,
messageListener,
this.messagingService,
this.logService,
);
this.syncServiceListener = new SyncServiceListener(
this.syncService,
messageListener,
this.messagingService,
this.logService,
);
}
this.eventUploadService = new EventUploadService(
this.apiService,
this.stateProvider,
@ -1111,122 +1080,128 @@ export default class MainBackground {
this.isSafari = this.platformUtilsService.isSafari();
// Background
if (!this.popupOnlyContext) {
this.fido2Background = new Fido2Background(
this.logService,
this.fido2ActiveRequestManager,
this.fido2ClientService,
this.vaultSettingsService,
this.scriptInjectorService,
this.configService,
this.authService,
);
const lockService = new DefaultLockService(this.accountService, this.vaultTimeoutService);
this.fido2Background = new Fido2Background(
this.logService,
this.fido2ActiveRequestManager,
this.fido2ClientService,
this.vaultSettingsService,
this.scriptInjectorService,
this.configService,
this.authService,
);
this.runtimeBackground = new RuntimeBackground(
this,
this.autofillService,
this.platformUtilsService as BrowserPlatformUtilsService,
this.notificationsService,
this.autofillSettingsService,
this.processReloadService,
this.environmentService,
this.messagingService,
this.logService,
this.configService,
messageListener,
this.accountService,
lockService,
);
this.nativeMessagingBackground = new NativeMessagingBackground(
this.keyService,
this.encryptService,
this.cryptoFunctionService,
this.runtimeBackground,
this.messagingService,
this.appIdService,
this.platformUtilsService,
this.logService,
this.authService,
this.biometricStateService,
this.accountService,
);
this.commandsBackground = new CommandsBackground(
this,
this.platformUtilsService,
this.vaultTimeoutService,
this.authService,
() => this.generatePasswordToClipboard(),
);
this.notificationBackground = new NotificationBackground(
this.autofillService,
this.cipherService,
this.authService,
this.policyService,
this.folderService,
this.userNotificationSettingsService,
this.domainSettingsService,
this.environmentService,
this.logService,
this.themeStateService,
this.configService,
this.accountService,
);
const lockService = new DefaultLockService(this.accountService, this.vaultTimeoutService);
this.overlayNotificationsBackground = new OverlayNotificationsBackground(
this.logService,
this.configService,
this.notificationBackground,
);
this.runtimeBackground = new RuntimeBackground(
this,
this.autofillService,
this.platformUtilsService as BrowserPlatformUtilsService,
this.notificationsService,
this.autofillSettingsService,
this.processReloadService,
this.environmentService,
this.messagingService,
this.logService,
this.configService,
messageListener,
this.accountService,
lockService,
);
this.nativeMessagingBackground = new NativeMessagingBackground(
this.keyService,
this.encryptService,
this.cryptoFunctionService,
this.runtimeBackground,
this.messagingService,
this.appIdService,
this.platformUtilsService,
this.logService,
this.authService,
this.biometricStateService,
this.accountService,
);
this.commandsBackground = new CommandsBackground(
this,
this.platformUtilsService,
this.vaultTimeoutService,
this.authService,
() => this.generatePasswordToClipboard(),
);
this.notificationBackground = new NotificationBackground(
this.autofillService,
this.cipherService,
this.authService,
this.policyService,
this.folderService,
this.userNotificationSettingsService,
this.domainSettingsService,
this.environmentService,
this.logService,
this.themeStateService,
this.configService,
this.accountService,
);
this.filelessImporterBackground = new FilelessImporterBackground(
this.configService,
this.authService,
this.policyService,
this.notificationBackground,
this.importService,
this.syncService,
this.scriptInjectorService,
);
this.overlayNotificationsBackground = new OverlayNotificationsBackground(
this.logService,
this.configService,
this.notificationBackground,
);
this.autoSubmitLoginBackground = new AutoSubmitLoginBackground(
this.logService,
this.autofillService,
this.scriptInjectorService,
this.authService,
this.configService,
this.platformUtilsService,
this.policyService,
);
this.filelessImporterBackground = new FilelessImporterBackground(
this.configService,
this.authService,
this.policyService,
this.notificationBackground,
this.importService,
this.syncService,
this.scriptInjectorService,
);
const contextMenuClickedHandler = new ContextMenuClickedHandler(
(options) => this.platformUtilsService.copyToClipboard(options.text),
async () => this.generatePasswordToClipboard(),
async (tab, cipher) => {
this.loginToAutoFill = cipher;
if (tab == null) {
return;
}
this.autoSubmitLoginBackground = new AutoSubmitLoginBackground(
this.logService,
this.autofillService,
this.scriptInjectorService,
this.authService,
this.configService,
this.platformUtilsService,
this.policyService,
);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
BrowserApi.tabSendMessage(tab, {
command: "collectPageDetails",
tab: tab,
sender: "contextMenu",
});
},
this.authService,
this.cipherService,
this.totpService,
this.eventCollectionService,
this.userVerificationService,
this.accountService,
);
const contextMenuClickedHandler = new ContextMenuClickedHandler(
(options) => this.platformUtilsService.copyToClipboard(options.text),
async (_tab) => {
const options = (await this.passwordGenerationService.getOptions())?.[0] ?? {};
const password = await this.passwordGenerationService.generatePassword(options);
this.platformUtilsService.copyToClipboard(password);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.passwordGenerationService.addHistory(password);
},
async (tab, cipher) => {
this.loginToAutoFill = cipher;
if (tab == null) {
return;
}
this.contextMenusBackground = new ContextMenusBackground(contextMenuClickedHandler);
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
BrowserApi.tabSendMessage(tab, {
command: "collectPageDetails",
tab: tab,
sender: "contextMenu",
});
},
this.authService,
this.cipherService,
this.totpService,
this.eventCollectionService,
this.userVerificationService,
this.accountService,
);
this.contextMenusBackground = new ContextMenusBackground(contextMenuClickedHandler);
this.idleBackground = new IdleBackground(
this.vaultTimeoutService,
@ -1245,29 +1220,27 @@ export default class MainBackground {
this.stateProvider,
);
if (!this.popupOnlyContext) {
this.mainContextMenuHandler = new MainContextMenuHandler(
this.stateService,
this.autofillSettingsService,
this.i18nService,
this.logService,
this.billingAccountProfileStateService,
);
this.mainContextMenuHandler = new MainContextMenuHandler(
this.stateService,
this.autofillSettingsService,
this.i18nService,
this.logService,
this.billingAccountProfileStateService,
);
this.cipherContextMenuHandler = new CipherContextMenuHandler(
this.mainContextMenuHandler,
this.authService,
this.cipherContextMenuHandler = new CipherContextMenuHandler(
this.mainContextMenuHandler,
this.authService,
this.cipherService,
);
if (chrome.webRequest != null && chrome.webRequest.onAuthRequired != null) {
this.webRequestBackground = new WebRequestBackground(
this.platformUtilsService,
this.cipherService,
this.authService,
chrome.webRequest,
);
if (chrome.webRequest != null && chrome.webRequest.onAuthRequired != null) {
this.webRequestBackground = new WebRequestBackground(
this.platformUtilsService,
this.cipherService,
this.authService,
chrome.webRequest,
);
}
}
this.userAutoUnlockKeyService = new UserAutoUnlockKeyService(this.keyService);
@ -1282,7 +1255,7 @@ export default class MainBackground {
this.containerService.attachToGlobal(self);
// Only the "true" background should run migrations
await this.stateService.init({ runMigrations: !this.popupOnlyContext });
await this.stateService.init({ runMigrations: true });
// This is here instead of in in the InitService b/c we don't plan for
// side effects to run in the Browser InitService.
@ -1304,10 +1277,6 @@ export default class MainBackground {
this.popupViewCacheBackgroundService.startObservingTabChanges();
if (this.popupOnlyContext) {
return;
}
await this.vaultTimeoutService.init(true);
this.fido2Background.init();
await this.runtimeBackground.init();
@ -1636,7 +1605,6 @@ export default class MainBackground {
*/
async initOverlayAndTabsBackground() {
if (
this.popupOnlyContext ||
this.overlayBackground ||
this.tabsBackground ||
(await firstValueFrom(this.authService.activeAccountStatus$)) ===
@ -1677,6 +1645,7 @@ export default class MainBackground {
this.fido2ActiveRequestManager,
inlineMenuFieldQualificationService,
this.themeStateService,
this.totpService,
() => this.generatePassword(),
(password) => this.addPasswordToHistory(password),
);

View File

@ -274,7 +274,11 @@ export class NativeMessagingBackground {
let message = rawMessage as ReceiveMessage;
if (!this.platformUtilsService.isSafari()) {
message = JSON.parse(
await this.encryptService.decryptToUtf8(rawMessage as EncString, this.sharedSecret),
await this.encryptService.decryptToUtf8(
rawMessage as EncString,
this.sharedSecret,
"ipc-desktop-ipc-channel-key",
),
);
}

View File

@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "__MSG_extName__",
"short_name": "__MSG_appName__",
"version": "2024.10.1",
"version": "2024.11.0",
"description": "__MSG_extDesc__",
"default_locale": "en",
"author": "Bitwarden Inc.",

View File

@ -3,7 +3,7 @@
"minimum_chrome_version": "102.0",
"name": "__MSG_extName__",
"short_name": "__MSG_appName__",
"version": "2024.10.1",
"version": "2024.11.0",
"description": "__MSG_extDesc__",
"default_locale": "en",
"author": "Bitwarden Inc.",

View File

@ -2,29 +2,6 @@ import { ConsoleLogService } from "@bitwarden/common/platform/services/console-l
import MainBackground from "../background/main.background";
import { BrowserApi } from "./browser/browser-api";
const logService = new ConsoleLogService(false);
if (BrowserApi.isManifestVersion(3)) {
startHeartbeat().catch((error) => logService.error(error));
}
const bitwardenMain = ((self as any).bitwardenMain = new MainBackground());
bitwardenMain.bootstrap().catch((error) => logService.error(error));
/**
* Tracks when a service worker was last alive and extends the service worker
* lifetime by writing the current time to extension storage every 20 seconds.
*/
async function runHeartbeat() {
await chrome.storage.local.set({ "last-heartbeat": new Date().getTime() });
}
/**
* Starts the heartbeat interval which keeps the service worker alive.
*/
async function startHeartbeat() {
// Run the heartbeat once at service worker startup, then again every 20 seconds.
runHeartbeat()
.then(() => setInterval(runHeartbeat, 20 * 1000))
.catch((error) => logService.error(error));
}

View File

@ -58,11 +58,33 @@ export class BrowserApi {
}
static async createWindow(options: chrome.windows.CreateData): Promise<chrome.windows.Window> {
return new Promise((resolve) =>
chrome.windows.create(options, (window) => {
resolve(window);
}),
);
return new Promise((resolve) => {
chrome.windows.create(options, async (newWindow) => {
if (!BrowserApi.isSafariApi) {
return resolve(newWindow);
}
// Safari doesn't close the default extension popup when a new window is created so we need to
// manually trigger the close by focusing the main window after the new window is created
const allWindows = await new Promise<chrome.windows.Window[]>((resolve) => {
chrome.windows.getAll({ windowTypes: ["normal"] }, (windows) => resolve(windows));
});
const mainWindow = allWindows.find((window) => window.id !== newWindow.id);
// No main window found, resolve the new window
if (mainWindow == null || !mainWindow.id) {
return resolve(newWindow);
}
// Focus the main window to close the extension popup
chrome.windows.update(mainWindow.id, { focused: true }, () => {
// Refocus the newly created window
chrome.windows.update(newWindow.id, { focused: true }, () => {
resolve(newWindow);
});
});
});
});
}
/**

View File

@ -24,8 +24,8 @@ import {
LockComponentService,
} from "@bitwarden/auth/angular";
import { LockService, LoginEmailService, PinServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
@ -92,9 +92,15 @@ import { InlineDerivedStateProvider } from "@bitwarden/common/platform/state/imp
import { PrimarySecondaryStorageService } from "@bitwarden/common/platform/storage/primary-secondary-storage.service";
import { WindowStorageService } from "@bitwarden/common/platform/storage/window-storage.service";
import { SyncService } from "@bitwarden/common/platform/sync";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
import { InternalSendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService as FolderServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
import {
FolderService as FolderServiceAbstraction,
InternalFolderService,
} from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
import { TotpService } from "@bitwarden/common/vault/services/totp.service";
import { DialogService, ToastService } from "@bitwarden/components";
@ -107,7 +113,6 @@ import { ExtensionAnonLayoutWrapperDataService } from "../../auth/popup/extensio
import { ExtensionLoginComponentService } from "../../auth/popup/login/extension-login-component.service";
import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service";
import AutofillService from "../../autofill/services/autofill.service";
import MainBackground from "../../background/main.background";
import { ForegroundBrowserBiometricsService } from "../../key-management/biometrics/foreground-browser-biometrics";
import { BrowserKeyService } from "../../key-management/browser-key.service";
import { BrowserApi } from "../../platform/browser/browser-api";
@ -117,12 +122,12 @@ import { ChromeMessageSender } from "../../platform/messaging/chrome-message.sen
/* eslint-enable no-restricted-imports */
import { OffscreenDocumentService } from "../../platform/offscreen-document/abstractions/offscreen-document";
import { DefaultOffscreenDocumentService } from "../../platform/offscreen-document/offscreen-document.service";
import BrowserPopupUtils from "../../platform/popup/browser-popup-utils";
import { BrowserFileDownloadService } from "../../platform/popup/services/browser-file-download.service";
import { PopupViewCacheService } from "../../platform/popup/view-cache/popup-view-cache.service";
import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service";
import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service";
import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service";
import BrowserMemoryStorageService from "../../platform/services/browser-memory-storage.service";
import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service";
import I18nService from "../../platform/services/i18n.service";
import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service";
@ -130,6 +135,7 @@ import { BrowserSdkClientFactory } from "../../platform/services/sdk/browser-sdk
import { ForegroundTaskSchedulerService } from "../../platform/services/task-scheduler/foreground-task-scheduler.service";
import { BrowserStorageServiceProvider } from "../../platform/storage/browser-storage-service.provider";
import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service";
import { ForegroundSyncService } from "../../platform/sync/foreground-sync.service";
import { fromChromeRuntimeMessaging } from "../../platform/utils/from-chrome-runtime-messaging";
import { ExtensionLockComponentService } from "../../services/extension-lock-component.service";
import { ForegroundVaultTimeoutService } from "../../services/vault-timeout/foreground-vault-timeout.service";
@ -151,26 +157,6 @@ const DISK_BACKUP_LOCAL_STORAGE = new SafeInjectionToken<
AbstractStorageService & ObservableStorageService
>("DISK_BACKUP_LOCAL_STORAGE");
const needsBackgroundInit = BrowserPopupUtils.backgroundInitializationRequired();
const mainBackground: MainBackground = needsBackgroundInit
? createLocalBgService()
: BrowserApi.getBackgroundPage().bitwardenMain;
function createLocalBgService() {
const localBgService = new MainBackground(true);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
localBgService.bootstrap();
return localBgService;
}
/** @deprecated This method needs to be removed as part of MV3 conversion. Please do not add more and actively try to remove usages */
function getBgService<T>(service: keyof MainBackground) {
return (): T => {
return mainBackground ? (mainBackground[service] as any as T) : null;
};
}
/**
* Provider definitions used in the ngModule.
* Add your provider definition here using the safeProvider function as a wrapper. This will give you type safety.
@ -307,8 +293,23 @@ const safeProviders: SafeProvider[] = [
}),
safeProvider({
provide: SyncService,
useFactory: getBgService<SyncService>("syncService"),
deps: [],
useClass: ForegroundSyncService,
deps: [
StateService,
InternalFolderService,
FolderApiServiceAbstraction,
MessageSender,
LogService,
CipherService,
CollectionService,
ApiService,
AccountServiceAbstraction,
AuthService,
InternalSendService,
SendApiService,
MessageListener,
StateProvider,
],
}),
safeProvider({
provide: DomainSettingsService,
@ -358,11 +359,6 @@ const safeProviders: SafeProvider[] = [
useClass: ForegroundVaultTimeoutService,
deps: [MessagingServiceAbstraction],
}),
safeProvider({
provide: NotificationsService,
useFactory: getBgService<NotificationsService>("notificationsService"),
deps: [],
}),
safeProvider({
provide: VaultFilterService,
useClass: VaultFilterService,
@ -382,8 +378,8 @@ const safeProviders: SafeProvider[] = [
}),
safeProvider({
provide: MEMORY_STORAGE,
useFactory: getBgService<AbstractStorageService>("memoryStorageService"),
deps: [],
useFactory: (memoryStorage: AbstractStorageService) => memoryStorage,
deps: [OBSERVABLE_MEMORY_STORAGE],
}),
safeProvider({
provide: OBSERVABLE_MEMORY_STORAGE,
@ -392,9 +388,7 @@ const safeProviders: SafeProvider[] = [
return new ForegroundMemoryStorageService();
}
return getBgService<AbstractStorageService & ObservableStorageService>(
"memoryStorageForStateProviders",
)();
return new BrowserMemoryStorageService();
},
deps: [],
}),
@ -407,9 +401,7 @@ const safeProviders: SafeProvider[] = [
return regularMemoryStorageService;
}
return getBgService<AbstractStorageService & ObservableStorageService>(
"largeObjectMemoryStorageForStateProviders",
)();
return new ForegroundMemoryStorageService();
},
deps: [OBSERVABLE_MEMORY_STORAGE],
}),
@ -494,15 +486,7 @@ const safeProviders: SafeProvider[] = [
}),
safeProvider({
provide: INTRAPROCESS_MESSAGING_SUBJECT,
useFactory: () => {
if (BrowserPopupUtils.backgroundInitializationRequired()) {
// There is no persistent main background which means we have one in memory,
// we need the same instance that our in memory background is utilizing.
return getBgService("intraprocessMessagingSubject")();
} else {
return new Subject<Message<Record<string, unknown>>>();
}
},
useFactory: () => new Subject<Message<Record<string, unknown>>>(),
deps: [],
}),
safeProvider({
@ -514,23 +498,6 @@ const safeProviders: SafeProvider[] = [
),
deps: [INTRAPROCESS_MESSAGING_SUBJECT, LogService],
}),
safeProvider({
provide: INTRAPROCESS_MESSAGING_SUBJECT,
useFactory: () => {
if (needsBackgroundInit) {
// We will have created a popup within this context, in that case
// we want to make sure we have the same subject as that context so we
// can message with it.
return getBgService("intraprocessMessagingSubject")();
} else {
// There isn't a locally created background so we will communicate with
// the true background through chrome apis, in that case, we can just create
// one for ourself.
return new Subject<Message<Record<string, unknown>>>();
}
},
deps: [],
}),
safeProvider({
provide: DISK_BACKUP_LOCAL_STORAGE,
useFactory: (diskStorage: AbstractStorageService & ObservableStorageService) =>
@ -572,13 +539,7 @@ const safeProviders: SafeProvider[] = [
}),
safeProvider({
provide: ForegroundTaskSchedulerService,
useFactory: (logService: LogService, stateProvider: StateProvider) => {
if (needsBackgroundInit) {
return getBgService<ForegroundTaskSchedulerService>("taskSchedulerService")();
}
return new ForegroundTaskSchedulerService(logService, stateProvider);
},
useClass: ForegroundTaskSchedulerService,
deps: [LogService, StateProvider],
}),
safeProvider({

View File

@ -24,9 +24,9 @@ import {
SendFormConfig,
SendFormConfigService,
SendFormMode,
SendFormModule,
} from "@bitwarden/send-ui";
import { SendFormModule } from "../../../../../../../libs/tools/send/send-ui/src/send-form/send-form.module";
import { PopupFooterComponent } from "../../../../platform/popup/layout/popup-footer.component";
import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component";

View File

@ -118,7 +118,7 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Name" xml:space="preserve">
<value>Bitwarden Password Manager</value>
<value>Bitwarden passordbehandler</value>
</data>
<data name="Summary" xml:space="preserve">
<value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value>

View File

@ -1,7 +1,7 @@
{
"name": "@bitwarden/cli",
"description": "A secure and free password manager for all of your devices.",
"version": "2024.10.0",
"version": "2024.11.0",
"keywords": [
"bitwarden",
"password",
@ -58,7 +58,7 @@
"dependencies": {
"@koa/multer": "3.0.2",
"@koa/router": "13.1.0",
"argon2": "0.40.1",
"argon2": "0.41.1",
"big-integer": "1.6.52",
"browser-hrtime": "1.1.8",
"chalk": "4.1.2",
@ -80,7 +80,7 @@
"papaparse": "5.4.1",
"proper-lockfile": "4.1.2",
"rxjs": "7.8.1",
"tldts": "6.1.56",
"tldts": "6.1.58",
"zxcvbn": "4.4.2"
}
}

View File

@ -68,7 +68,7 @@ export class UnlockCommand {
return Response.error(e.message);
}
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey, userId);
await this.keyService.setUserKey(userKey, userId);
if (await this.keyConnectorService.getConvertAccountRequired()) {

View File

@ -404,6 +404,7 @@ export class ServiceContainer {
this.stateService,
this.keyGenerationService,
this.encryptService,
this.logService,
);
this.kdfConfigService = new KdfConfigService(this.stateProvider);

View File

@ -4,18 +4,18 @@ version = 3
[[package]]
name = "addr2line"
version = "0.22.0"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678"
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
dependencies = [
"gimli",
]
[[package]]
name = "adler"
version = "1.0.2"
name = "adler2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "aes"
@ -39,9 +39,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.86"
version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775"
[[package]]
name = "arboard"
@ -216,17 +216,17 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "backtrace"
version = "0.3.73"
version = "0.3.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a"
checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
dependencies = [
"addr2line",
"cc",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
"windows-targets 0.52.6",
]
[[package]]
@ -289,9 +289,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.7.2"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
[[package]]
name = "cbc"
@ -304,9 +304,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.1.28"
version = "1.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e80e3b6a3ab07840e1cae9b0666a63970dc28e8ed5ffbcdacbfc760c281bfc1"
checksum = "67b9470d453346108f93a59222a9a1a5724db32d0a4727b7ab7ace4b4d822dc9"
dependencies = [
"shlex",
]
@ -439,9 +439,9 @@ dependencies = [
[[package]]
name = "cxx"
version = "1.0.128"
version = "1.0.129"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54ccead7d199d584d139148b04b4a368d1ec7556a1d9ea2548febb1b9d49f9a4"
checksum = "cbdc8cca144dce1c4981b5c9ab748761619979e515c3d53b5df385c677d1d007"
dependencies = [
"cc",
"cxxbridge-flags",
@ -451,9 +451,9 @@ dependencies = [
[[package]]
name = "cxx-build"
version = "1.0.128"
version = "1.0.129"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c77953e99f01508f89f55c494bfa867171ef3a6c8cea03d26975368f2121a5c1"
checksum = "c5764c3142ab44fcf857101d12c0ddf09c34499900557c764f5ad0597159d1fc"
dependencies = [
"cc",
"codespan-reporting",
@ -466,15 +466,15 @@ dependencies = [
[[package]]
name = "cxxbridge-flags"
version = "1.0.128"
version = "1.0.129"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65777e06cc48f0cb0152024c77d6cf9e4bdb4408e7b48bea993d42fa0f5b02b6"
checksum = "d422aff542b4fa28c2ce8e5cc202d42dbf24702345c1fba3087b2d3f8a1b90ff"
[[package]]
name = "cxxbridge-macro"
version = "1.0.128"
version = "1.0.129"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98532a60dedaebc4848cb2cba5023337cc9ea3af16a5b062633fabfd9f18fb60"
checksum = "a1719100f31492cd6adeeab9a0f46cdbc846e615fdb66d7b398aa46ec7fdd06f"
dependencies = [
"proc-macro2",
"quote",
@ -546,6 +546,7 @@ dependencies = [
"napi-derive",
"tokio",
"tokio-util",
"windows-registry",
]
[[package]]
@ -758,9 +759,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-lite"
version = "2.3.0"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5"
checksum = "3f1fa2f9765705486b33fd2acf1577f8ec449c2ba1f318ae5447697b7c08d210"
dependencies = [
"fastrand",
"futures-core",
@ -843,9 +844,9 @@ dependencies = [
[[package]]
name = "gimli"
version = "0.29.0"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "gio"
@ -936,9 +937,9 @@ dependencies = [
[[package]]
name = "hashbrown"
version = "0.15.0"
version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb"
checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3"
[[package]]
name = "heck"
@ -964,15 +965,6 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "home"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "indexmap"
version = "2.6.0"
@ -1141,29 +1133,30 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.7.4"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08"
checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
dependencies = [
"adler",
"adler2",
]
[[package]]
name = "mio"
version = "0.8.11"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
dependencies = [
"hermit-abi 0.3.9",
"libc",
"wasi",
"windows-sys 0.48.0",
"windows-sys 0.52.0",
]
[[package]]
name = "napi"
version = "2.16.11"
version = "2.16.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53575dfa17f208dd1ce3a2da2da4659aae393b256a472f2738a8586a6c4107fd"
checksum = "214f07a80874bb96a8433b3cdfc84980d56c7b02e1a0d7ba4ba0db5cef785e2b"
dependencies = [
"bitflags",
"ctor",
@ -1258,16 +1251,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num_cpus"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
"hermit-abi 0.3.9",
"libc",
]
[[package]]
name = "num_threads"
version = "0.1.7"
@ -1458,9 +1441,9 @@ dependencies = [
[[package]]
name = "pin-project-lite"
version = "0.2.14"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
[[package]]
name = "pin-utils"
@ -1526,9 +1509,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.87"
version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a"
checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
dependencies = [
"unicode-ident",
]
@ -1609,9 +1592,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.11.0"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
@ -1653,9 +1636,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustix"
version = "0.38.34"
version = "0.38.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811"
dependencies = [
"bitflags",
"errno",
@ -1713,18 +1696,18 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
[[package]]
name = "serde"
version = "1.0.210"
version = "1.0.214"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.210"
version = "1.0.214"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766"
dependencies = [
"proc-macro2",
"quote",
@ -1832,9 +1815,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "syn"
version = "2.0.79"
version = "2.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
dependencies = [
"proc-macro2",
"quote",
@ -1862,9 +1845,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "tempfile"
version = "3.12.0"
version = "3.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64"
checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b"
dependencies = [
"cfg-if",
"fastrand",
@ -1884,18 +1867,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.61"
version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.61"
version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e"
dependencies = [
"proc-macro2",
"quote",
@ -1937,26 +1920,25 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.38.0"
version = "1.41.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb"
dependencies = [
"backtrace",
"bytes",
"libc",
"mio",
"num_cpus",
"pin-project-lite",
"socket2",
"tokio-macros",
"windows-sys 0.48.0",
"windows-sys 0.52.0",
]
[[package]]
name = "tokio-macros"
version = "2.3.0"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
dependencies = [
"proc-macro2",
"quote",
@ -2043,12 +2025,11 @@ dependencies = [
[[package]]
name = "tree_magic_mini"
version = "3.1.5"
version = "3.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "469a727cac55b41448315cc10427c069c618ac59bb6a4480283fcd811749bdc2"
checksum = "aac5e8971f245c3389a5a76e648bfc80803ae066a1243a75db0064d7c1129d63"
dependencies = [
"fnv",
"home",
"memchr",
"nom",
"once_cell",
@ -2124,9 +2105,9 @@ dependencies = [
[[package]]
name = "wayland-client"
version = "0.31.6"
version = "0.31.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3f45d1222915ef1fd2057220c1d9d9624b7654443ea35c3877f7a52bd0a5a2d"
checksum = "b66249d3fc69f76fd74c82cc319300faa554e9d865dab1f7cd66cc20db10b280"
dependencies = [
"bitflags",
"rustix",
@ -2236,7 +2217,7 @@ checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
dependencies = [
"windows-implement",
"windows-interface",
"windows-result",
"windows-result 0.1.2",
"windows-targets 0.52.6",
]
@ -2262,6 +2243,17 @@ dependencies = [
"syn",
]
[[package]]
name = "windows-registry"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bafa604f2104cf5ae2cc2db1dee84b7e6a5d11b05f737b60def0ffdc398cbc0a"
dependencies = [
"windows-result 0.2.0",
"windows-strings",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-result"
version = "0.1.2"
@ -2271,6 +2263,24 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-result"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-strings"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978d65aedf914c664c510d9de43c8fd85ca745eaff1ed53edf409b479e441663"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
@ -2477,9 +2487,9 @@ dependencies = [
[[package]]
name = "zbus"
version = "4.3.1"
version = "4.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "851238c133804e0aa888edf4a0229481c753544ca12a60fd1c3230c8a500fe40"
checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725"
dependencies = [
"async-broadcast",
"async-executor",
@ -2515,9 +2525,9 @@ dependencies = [
[[package]]
name = "zbus_macros"
version = "4.3.1"
version = "4.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d5a3f12c20bd473be3194af6b49d50d7bb804ef3192dc70eddedb26b85d9da7"
checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e"
dependencies = [
"proc-macro-crate",
"proc-macro2",
@ -2573,9 +2583,9 @@ dependencies = [
[[package]]
name = "zvariant"
version = "4.1.2"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1724a2b330760dc7d2a8402d841119dc869ef120b139d29862d6980e9c75bfc9"
checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe"
dependencies = [
"endi",
"enumflags2",
@ -2586,9 +2596,9 @@ dependencies = [
[[package]]
name = "zvariant_derive"
version = "4.1.2"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55025a7a518ad14518fb243559c058a2e5b848b015e31f1d90414f36e3317859"
checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449"
dependencies = [
"proc-macro-crate",
"proc-macro2",
@ -2599,9 +2609,9 @@ dependencies = [
[[package]]
name = "zvariant_utils"
version = "2.0.0"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc242db087efc22bd9ade7aa7809e4ba828132edc312871584a6b4391bdf8786"
checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340"
dependencies = [
"proc-macro2",
"quote",

View File

@ -23,7 +23,7 @@ sys = [
[dependencies]
aes = "=0.8.4"
anyhow = "=1.0.86"
anyhow = "=1.0.93"
arboard = { version = "=3.4.1", default-features = false, features = [
"wayland-data-control",
] }
@ -38,8 +38,8 @@ rand = "=0.8.5"
retry = "=2.0.0"
scopeguard = "=1.2.0"
sha2 = "=0.10.8"
thiserror = "=1.0.61"
tokio = { version = "=1.38.0", features = ["io-util", "sync", "macros"] }
thiserror = "=1.0.68"
tokio = { version = "=1.41.0", features = ["io-util", "sync", "macros"] }
tokio-util = "=0.7.12"
typenum = "=1.17.0"
@ -68,5 +68,5 @@ security-framework-sys = { version = "=2.11.0", optional = true }
[target.'cfg(target_os = "linux")'.dependencies]
gio = { version = "=0.19.5", optional = true }
libsecret = { version = "=0.5.0", optional = true }
zbus = { version = "=4.3.1", optional = true }
zbus = { version = "=4.4.0", optional = true }
zbus_polkit = { version = "=4.0.0", optional = true }

View File

@ -31,7 +31,7 @@ pub fn path(name: &str) -> std::path::PathBuf {
format!(r"\\.\pipe\{hash_b64}.app.{name}").into()
}
#[cfg(target_os = "macos")]
#[cfg(all(target_os = "macos", not(debug_assertions)))]
{
let mut home = dirs::home_dir().unwrap();
@ -53,6 +53,13 @@ pub fn path(name: &str) -> std::path::PathBuf {
tmp.join(format!("app.{name}"))
}
#[cfg(all(target_os = "macos", debug_assertions))]
{
// When running in debug mode, we use the tmp dir because the app is not sandboxed
let dir = std::env::temp_dir();
dir.join(format!("app.{name}"))
}
#[cfg(target_os = "linux")]
{
// On Linux, we use the user's cache directory.

View File

@ -14,12 +14,15 @@ default = []
manual_test = []
[dependencies]
anyhow = "=1.0.86"
anyhow = "=1.0.93"
desktop_core = { path = "../core" }
napi = { version = "=2.16.11", features = ["async"] }
napi = { version = "=2.16.13", features = ["async"] }
napi-derive = "=2.16.12"
tokio = { version = "1.38.0" }
tokio-util = "0.7.11"
[target.'cfg(windows)'.dependencies]
windows-registry = "=0.3.0"
[build-dependencies]
napi-build = "=2.1.3"

View File

@ -51,6 +51,10 @@ export declare namespace powermonitors {
export function onLock(callback: (err: Error | null, ) => any): Promise<void>
export function isLockMonitorAvailable(): Promise<boolean>
}
export declare namespace windows_registry {
export function createKey(key: string, subkey: string, value: string): Promise<void>
export function deleteKey(key: string, subkey: string): Promise<void>
}
export declare namespace ipc {
export interface IpcMessage {
clientId: number

View File

@ -1,5 +1,8 @@
#[macro_use]
extern crate napi_derive;
mod registry;
#[napi]
pub mod passwords {
/// Fetch the stored password from the keychain.
@ -190,6 +193,21 @@ pub mod powermonitors {
}
#[napi]
pub mod windows_registry {
#[napi]
pub async fn create_key(key: String, subkey: String, value: String) -> napi::Result<()> {
crate::registry::create_key(&key, &subkey, &value)
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[napi]
pub async fn delete_key(key: String, subkey: String) -> napi::Result<()> {
crate::registry::delete_key(&key, &subkey)
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
}
#[napi]
pub mod ipc {
use desktop_core::ipc::server::{Message, MessageType};

View File

@ -0,0 +1,9 @@
use anyhow::{bail, Result};
pub fn create_key(_key: &str, _subkey: &str, _value: &str) -> Result<()> {
bail!("Not implemented")
}
pub fn delete_key(_key: &str, _subkey: &str) -> Result<()> {
bail!("Not implemented")
}

View File

@ -0,0 +1,4 @@
#[cfg_attr(target_os = "windows", path = "windows.rs")]
#[cfg_attr(not(target_os = "windows"), path = "dummy.rs")]
mod internal;
pub use internal::*;

View File

@ -0,0 +1,29 @@
use anyhow::{bail, Result};
fn convert_key(key: &str) -> Result<&'static windows_registry::Key> {
Ok(match key.to_uppercase().as_str() {
"HKEY_CURRENT_USER" | "HKCU" => windows_registry::CURRENT_USER,
"HKEY_LOCAL_MACHINE" | "HKLM" => windows_registry::LOCAL_MACHINE,
"HKEY_CLASSES_ROOT" | "HKCR" => windows_registry::CLASSES_ROOT,
_ => bail!("Invalid key"),
})
}
pub fn create_key(key: &str, subkey: &str, value: &str) -> Result<()> {
let key = convert_key(key)?;
let subkey = key.create(subkey)?;
const DEFAULT: &str = "";
subkey.set_string(DEFAULT, value)?;
Ok(())
}
pub fn delete_key(key: &str, subkey: &str) -> Result<()> {
let key = convert_key(key)?;
key.remove_tree(subkey)?;
Ok(())
}

View File

@ -7,7 +7,7 @@ version = "0.0.0"
publish = false
[dependencies]
anyhow = "=1.0.86"
anyhow = "=1.0.93"
desktop_core = { path = "../core", default-features = false }
futures = "0.3.30"
log = "0.4.22"

View File

@ -90,13 +90,6 @@
"electronUpdaterCompatibility": ">=0.0.1",
"target": ["portable", "nsis-web", "appx"],
"sign": "./sign.js",
"extraResources": [
{
"from": "../../node_modules/regedit/vbs",
"to": "regedit/vbs",
"filter": ["**/*"]
}
],
"extraFiles": [
{
"from": "desktop_native/dist/desktop_proxy.${platform}-${arch}.exe",

View File

@ -125,9 +125,9 @@
}
},
"node_modules/acorn": {
"version": "8.12.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
"license": "MIT",
"bin": {
"acorn": "bin/acorn"

View File

@ -219,7 +219,11 @@ export default class NativeMessageService {
key: string,
): Promise<DecryptedCommandData> {
const sharedKey = await this.getSharedKeyForKey(key);
const decrypted = await this.encryptService.decryptToUtf8(payload, sharedKey);
const decrypted = await this.encryptService.decryptToUtf8(
payload,
sharedKey,
"native-messaging-session",
);
return JSON.parse(decrypted);
}

View File

@ -1,7 +1,7 @@
{
"name": "@bitwarden/desktop",
"description": "A secure and free password manager for all of your devices.",
"version": "2024.10.3",
"version": "2024.11.0",
"keywords": [
"bitwarden",
"password",

View File

@ -58,30 +58,46 @@ async function run(context) {
id = identities[0].id;
}
console.log(`Signing proxy binary before the main bundle, using identity '${id}'`);
console.log(
`Signing proxy binary before the main bundle, using identity '${id}', for build ${context.electronPlatformName}`,
);
const appName = context.packager.appInfo.productFilename;
const appPath = `${context.appOutDir}/${appName}.app`;
const proxyPath = path.join(appPath, "Contents", "MacOS", "desktop_proxy");
const inheritProxyPath = path.join(appPath, "Contents", "MacOS", "desktop_proxy.inherit");
const packageId = "com.bitwarden.desktop";
const entitlementsName = "entitlements.desktop_proxy.plist";
const entitlementsPath = path.join(__dirname, "..", "resources", entitlementsName);
child_process.execSync(
`codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${entitlementsPath} ${proxyPath}`,
);
const inheritProxyPath = path.join(appPath, "Contents", "MacOS", "desktop_proxy.inherit");
const inheritEntitlementsName = "entitlements.desktop_proxy.inherit.plist";
const inheritEntitlementsPath = path.join(
__dirname,
"..",
"resources",
inheritEntitlementsName,
);
child_process.execSync(
`codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${inheritEntitlementsPath} ${inheritProxyPath}`,
);
if (is_mas) {
const entitlementsName = "entitlements.desktop_proxy.plist";
const entitlementsPath = path.join(__dirname, "..", "resources", entitlementsName);
child_process.execSync(
`codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${entitlementsPath} ${proxyPath}`,
);
const inheritEntitlementsName = "entitlements.desktop_proxy.inherit.plist";
const inheritEntitlementsPath = path.join(
__dirname,
"..",
"resources",
inheritEntitlementsName,
);
child_process.execSync(
`codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${inheritEntitlementsPath} ${inheritProxyPath}`,
);
} else {
// For non-Appstore builds, we don't need the inherit binary as they are not sandboxed,
// but we sign and include it anyway for consistency. It should be removed once DDG supports the proxy directly.
const entitlementsName = "entitlements.mac.plist";
const entitlementsPath = path.join(__dirname, "..", "resources", entitlementsName);
child_process.execSync(
`codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${entitlementsPath} ${proxyPath}`,
);
child_process.execSync(
`codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${entitlementsPath} ${inheritProxyPath}`,
);
}
}
}

View File

@ -626,7 +626,8 @@ export class SettingsComponent implements OnInit, OnDestroy {
async saveBrowserIntegration() {
if (
ipc.platform.deviceType === DeviceType.MacOsDesktop &&
!this.platformUtilsService.isMacAppStore()
!this.platformUtilsService.isMacAppStore() &&
!ipc.platform.isDev
) {
await this.dialogService.openSimpleDialog({
title: { key: "browserIntegrationUnsupportedTitle" },

View File

@ -1,7 +1,7 @@
import { Component, NgZone, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { Subject, takeUntil } from "rxjs";
import { Subject, takeUntil, tap } from "rxjs";
import { LoginComponentV1 as BaseLoginComponent } from "@bitwarden/angular/auth/components/login-v1.component";
import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service";
@ -14,8 +14,10 @@ import {
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -76,6 +78,7 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit, OnDe
webAuthnLoginService: WebAuthnLoginServiceAbstraction,
registerRouteService: RegisterRouteService,
toastService: ToastService,
private configService: ConfigService,
) {
super(
devicesApiService,
@ -105,6 +108,8 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit, OnDe
}
async ngOnInit() {
this.listenForUnauthUiRefreshFlagChanges();
await super.ngOnInit();
await this.getLoginWithDevice(this.loggedEmail);
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
@ -137,6 +142,29 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit, OnDe
this.componentDestroyed$.complete();
}
private listenForUnauthUiRefreshFlagChanges() {
this.configService
.getFeatureFlag$(FeatureFlag.UnauthenticatedExtensionUIRefresh)
.pipe(
tap(async (flag) => {
// If the flag is turned ON, we must force a reload to ensure the correct UI is shown
if (flag) {
const uniqueQueryParams = {
...this.route.queryParams,
// adding a unique timestamp to the query params to force a reload
t: new Date().getTime().toString(),
};
await this.router.navigate(["/"], {
queryParams: uniqueQueryParams,
});
}
}),
takeUntil(this.destroy$),
)
.subscribe();
}
async settings() {
const [modal, childComponent] = await this.modalService.openViewRef(
EnvironmentComponent,

View File

@ -61,7 +61,7 @@
}
},
"welcomeBack": {
"message": "Welcome back"
"message": "Добродошли назад"
},
"moveToOrgDesc": {
"message": "Изаберите организацију у коју желите да преместите ову ставку. Пребацивање у организацију преноси власништво над ставком тој организацији. Више нећете бити директни власник ове ставке када буде премештена."
@ -263,7 +263,7 @@
"message": "Генерисање лозинке"
},
"generatePassphrase": {
"message": "Generate passphrase"
"message": "Генеришите приступну фразу"
},
"type": {
"message": "Тип"
@ -401,7 +401,7 @@
"message": "Копирај лозинку"
},
"copyPassphrase": {
"message": "Copy passphrase",
"message": "Копирај приступну фразу",
"description": "Copy passphrase to clipboard"
},
"copyUri": {
@ -558,7 +558,7 @@
"message": "Креирај налог"
},
"newToBitwarden": {
"message": "New to Bitwarden?"
"message": "Нови сте у Bitwarden-у?"
},
"setAStrongPassword": {
"message": "Поставите јаку лозинку"
@ -570,16 +570,16 @@
"message": "Пријавите се"
},
"logInToBitwarden": {
"message": "Log in to Bitwarden"
"message": "Пријавите се на Bitwarden"
},
"logInWithPasskey": {
"message": "Log in with passkey"
"message": "Пријавите се са приступним кључем"
},
"loginWithDevice": {
"message": "Log in with device"
"message": "Пријавите се са уређајем"
},
"useSingleSignOn": {
"message": "Use single sign-on"
"message": "Употребити једнократну пријаву"
},
"submit": {
"message": "Пошаљи"
@ -854,7 +854,7 @@
"message": "УРЛ Сервера"
},
"selfHostBaseUrl": {
"message": "Self-host server URL",
"message": "УРЛ сервера који се самостално хостује",
"description": "Label for field requesting a self-hosted integration service URL"
},
"apiUrl": {
@ -1248,7 +1248,7 @@
"description": "Copy credit card number"
},
"copyEmail": {
"message": "Copy email"
"message": "Копирати имејл"
},
"copySecurityCode": {
"message": "Копирај сигурносни код",
@ -1684,10 +1684,10 @@
"message": "Брисање налога је трајно. Не може се поништити."
},
"cannotDeleteAccount": {
"message": "Cannot delete account"
"message": "Није могуће избрисати налог"
},
"cannotDeleteAccountDesc": {
"message": "This action cannot be completed because your account is owned by an organization. Contact your organization administrator for additional details."
"message": "Ова радња се не може довршити јер је ваш налог у власништву организације. Обратите се администратору своје организације за додатне детаље."
},
"accountDeleted": {
"message": "Налог обрисан"
@ -2391,10 +2391,10 @@
"message": "Генериши име"
},
"generateEmail": {
"message": "Generate email"
"message": "Генеришите имејл"
},
"generatorBoundariesHint": {
"message": "Value must be between $MIN$ and $MAX$",
"message": "Вредност мора бити између $MIN$ и $MAX$",
"description": "Explains spin box minimum and maximum values to the user",
"placeholders": {
"min": {
@ -2451,11 +2451,11 @@
"message": "Генеришите псеудоним е-поште помоћу екстерне услуге прослеђивања."
},
"forwarderDomainName": {
"message": "Email domain",
"message": "Домен имејла",
"description": "Labels the domain name email forwarder service option"
},
"forwarderDomainNameHint": {
"message": "Choose a domain that is supported by the selected service",
"message": "Изаберите домен који подржава изабрана услуга",
"description": "Guidance provided for email forwarding services that support multiple email domains."
},
"forwarderError": {
@ -2944,10 +2944,10 @@
"message": "Приступачни кључ"
},
"passkeyNotCopied": {
"message": "Приступачни кључ се неће копирати"
"message": "Приступни кључ неће бити копиран"
},
"passkeyNotCopiedAlert": {
"message": "Приступачни кључ неће бити копиран на клонирану ставку. Да ли желите да наставите са клонирањем ставке?"
"message": "Приступни кључ неће бити копиран на клонирану ставку. Да ли желите да наставите са клонирањем ставке?"
},
"aliasDomain": {
"message": "Домен алијаса"
@ -3168,10 +3168,10 @@
"message": "Омогућите хардверско убрзање и поново покрените"
},
"removePasskey": {
"message": "Уклонити приступачни кључ"
"message": "Уклонити приступни кључ"
},
"passkeyRemoved": {
"message": "Приступачни кључ је уклоњен"
"message": "Приступни кључ је уклоњен"
},
"errorAssigningTargetCollection": {
"message": "Грешка при додељивању циљне колекције."

View File

@ -61,7 +61,7 @@
}
},
"welcomeBack": {
"message": "Welcome back"
"message": "欢迎回来"
},
"moveToOrgDesc": {
"message": "选择一个您想将此项目移至的组织。移动到组织会将该项目的所有权转让给该组织。移动后,您将不再是此项目的直接所有者。"
@ -263,7 +263,7 @@
"message": "生成密码"
},
"generatePassphrase": {
"message": "Generate passphrase"
"message": "生成密码短语"
},
"type": {
"message": "类型"
@ -401,7 +401,7 @@
"message": "复制密码"
},
"copyPassphrase": {
"message": "Copy passphrase",
"message": "复制密码短语",
"description": "Copy passphrase to clipboard"
},
"copyUri": {
@ -558,7 +558,7 @@
"message": "创建账户"
},
"newToBitwarden": {
"message": "New to Bitwarden?"
"message": "Bitwarden 新手吗?"
},
"setAStrongPassword": {
"message": "设置强密码"
@ -570,16 +570,16 @@
"message": "登录"
},
"logInToBitwarden": {
"message": "Log in to Bitwarden"
"message": "登录到 Bitwarden"
},
"logInWithPasskey": {
"message": "Log in with passkey"
"message": "使用通行密钥登录"
},
"loginWithDevice": {
"message": "Log in with device"
"message": "使用设备登录"
},
"useSingleSignOn": {
"message": "Use single sign-on"
"message": "使用单点登录"
},
"submit": {
"message": "提交"
@ -854,7 +854,7 @@
"message": "服务器 URL"
},
"selfHostBaseUrl": {
"message": "Self-host server URL",
"message": "自托管服务器 URL",
"description": "Label for field requesting a self-hosted integration service URL"
},
"apiUrl": {
@ -1248,7 +1248,7 @@
"description": "Copy credit card number"
},
"copyEmail": {
"message": "Copy email"
"message": "复制电子邮件地址"
},
"copySecurityCode": {
"message": "复制安全码",
@ -1684,10 +1684,10 @@
"message": "删除账户是永久性操作,无法撤销!"
},
"cannotDeleteAccount": {
"message": "Cannot delete account"
"message": "无法删除账户"
},
"cannotDeleteAccountDesc": {
"message": "This action cannot be completed because your account is owned by an organization. Contact your organization administrator for additional details."
"message": "此操作无法完成,因为您的账户归组织所有。请联系您的组织管理员获取详细信息。"
},
"accountDeleted": {
"message": "账户已删除"
@ -2391,10 +2391,10 @@
"message": "生成用户名"
},
"generateEmail": {
"message": "Generate email"
"message": "生成邮件地址"
},
"generatorBoundariesHint": {
"message": "Value must be between $MIN$ and $MAX$",
"message": "值必须在 $MIN$ 和 $MAX$ 之间",
"description": "Explains spin box minimum and maximum values to the user",
"placeholders": {
"min": {
@ -2451,11 +2451,11 @@
"message": "使用外部转发服务生成一个电子邮件别名。"
},
"forwarderDomainName": {
"message": "Email domain",
"message": "邮件域名",
"description": "Labels the domain name email forwarder service option"
},
"forwarderDomainNameHint": {
"message": "Choose a domain that is supported by the selected service",
"message": "选择一个所选服务支持的域名",
"description": "Guidance provided for email forwarding services that support multiple email domains."
},
"forwarderError": {

View File

@ -1,12 +1,11 @@
import { existsSync, promises as fs } from "fs";
import { homedir, userInfo } from "os";
import * as path from "path";
import * as util from "util";
import { ipcMain } from "electron";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { ipc } from "@bitwarden/desktop-napi";
import { ipc, windows_registry } from "@bitwarden/desktop-napi";
import { isDev } from "../utils";
@ -132,18 +131,7 @@ export class NativeMessagingMain {
};
const chromeJson = {
...baseJson,
...{
allowed_origins: [
// Chrome extension
"chrome-extension://nngceckbapebfimnlniiiahkandclblb/",
// Chrome beta extension
"chrome-extension://hccnnhgbibccigepcmlgppchkpfdophk/",
// Edge extension
"chrome-extension://jbkfoedolllekgbhcbcoahefnbanhhlh/",
// Opera extension
"chrome-extension://ccnckbpmaceehanjmeomladnmlffdjgn/",
],
},
allowed_origins: await this.loadChromeIds(),
};
switch (process.platform) {
@ -153,12 +141,12 @@ export class NativeMessagingMain {
await this.writeManifest(path.join(destination, "chrome.json"), chromeJson);
const nmhs = this.getWindowsNMHS();
for (const [key, value] of Object.entries(nmhs)) {
for (const [name, [key, subkey]] of Object.entries(nmhs)) {
let manifestPath = path.join(destination, "chrome.json");
if (key === "Firefox") {
if (name === "Firefox") {
manifestPath = path.join(destination, "firefox.json");
}
await this.createWindowsRegistry(value, manifestPath);
await windows_registry.createKey(key, subkey, manifestPath);
}
break;
}
@ -180,35 +168,26 @@ export class NativeMessagingMain {
}
break;
}
case "linux":
if (existsSync(`${this.homedir()}/.mozilla/`)) {
await this.writeManifest(
`${this.homedir()}/.mozilla/native-messaging-hosts/com.8bit.bitwarden.json`,
firefoxJson,
);
}
if (existsSync(`${this.homedir()}/.config/google-chrome/`)) {
await this.writeManifest(
`${this.homedir()}/.config/google-chrome/NativeMessagingHosts/com.8bit.bitwarden.json`,
chromeJson,
);
}
if (existsSync(`${this.homedir()}/.config/microsoft-edge/`)) {
await this.writeManifest(
`${this.homedir()}/.config/microsoft-edge/NativeMessagingHosts/com.8bit.bitwarden.json`,
chromeJson,
);
}
if (existsSync(`${this.homedir()}/.config/chromium/`)) {
await this.writeManifest(
`${this.homedir()}/.config/chromium/NativeMessagingHosts/com.8bit.bitwarden.json`,
chromeJson,
);
case "linux": {
for (const [key, value] of Object.entries(this.getLinuxNMHS())) {
if (existsSync(value)) {
if (key === "Firefox") {
await this.writeManifest(
path.join(value, "native-messaging-hosts", "com.8bit.bitwarden.json"),
firefoxJson,
);
} else {
await this.writeManifest(
path.join(value, "NativeMessagingHosts", "com.8bit.bitwarden.json"),
chromeJson,
);
}
} else {
this.logService.warning(`${key} not found, skipping.`);
}
}
break;
}
default:
break;
}
@ -245,8 +224,8 @@ export class NativeMessagingMain {
await this.removeIfExists(path.join(this.userPath, "browsers", "chrome.json"));
const nmhs = this.getWindowsNMHS();
for (const [, value] of Object.entries(nmhs)) {
await this.deleteWindowsRegistry(value);
for (const [, [key, subkey]] of Object.entries(nmhs)) {
await windows_registry.deleteKey(key, subkey);
}
break;
}
@ -260,15 +239,18 @@ export class NativeMessagingMain {
break;
}
case "linux": {
await this.removeIfExists(
`${this.homedir()}/.mozilla/native-messaging-hosts/com.8bit.bitwarden.json`,
);
await this.removeIfExists(
`${this.homedir()}/.config/google-chrome/NativeMessagingHosts/com.8bit.bitwarden.json`,
);
await this.removeIfExists(
`${this.homedir()}/.config/microsoft-edge/NativeMessagingHosts/com.8bit.bitwarden.json`,
);
for (const [key, value] of Object.entries(this.getLinuxNMHS())) {
if (key === "Firefox") {
await this.removeIfExists(
path.join(value, "native-messaging-hosts", "com.8bit.bitwarden.json"),
);
} else {
await this.removeIfExists(
path.join(value, "NativeMessagingHosts", "com.8bit.bitwarden.json"),
);
}
}
break;
}
default:
@ -291,11 +273,14 @@ export class NativeMessagingMain {
private getWindowsNMHS() {
return {
Firefox: "HKCU\\SOFTWARE\\Mozilla\\NativeMessagingHosts\\com.8bit.bitwarden",
Chrome: "HKCU\\SOFTWARE\\Google\\Chrome\\NativeMessagingHosts\\com.8bit.bitwarden",
Chromium: "HKCU\\SOFTWARE\\Chromium\\NativeMessagingHosts\\com.8bit.bitwarden",
Firefox: ["HKCU", "SOFTWARE\\Mozilla\\NativeMessagingHosts\\com.8bit.bitwarden"],
Chrome: ["HKCU", "SOFTWARE\\Google\\Chrome\\NativeMessagingHosts\\com.8bit.bitwarden"],
Chromium: ["HKCU", "SOFTWARE\\Chromium\\NativeMessagingHosts\\com.8bit.bitwarden"],
// Edge uses the same registry key as Chrome as a fallback, but it's has its own separate key as well.
"Microsoft Edge": "HKCU\\SOFTWARE\\Microsoft\\Edge\\NativeMessagingHosts\\com.8bit.bitwarden",
"Microsoft Edge": [
"HKCU",
"SOFTWARE\\Microsoft\\Edge\\NativeMessagingHosts\\com.8bit.bitwarden",
],
};
}
@ -317,6 +302,15 @@ export class NativeMessagingMain {
/* eslint-enable no-useless-escape */
}
private getLinuxNMHS() {
return {
Firefox: `${this.homedir()}/.mozilla/`,
Chrome: `${this.homedir()}/.config/google-chrome/`,
Chromium: `${this.homedir()}/.config/chromium/`,
"Microsoft Edge": `${this.homedir()}/.config/microsoft-edge/`,
};
}
private async writeManifest(destination: string, manifest: object) {
this.logService.debug(`Writing manifest: ${destination}`);
@ -327,6 +321,83 @@ export class NativeMessagingMain {
await fs.writeFile(destination, JSON.stringify(manifest, null, 2));
}
private async loadChromeIds(): Promise<string[]> {
const ids: Set<string> = new Set([
// Chrome extension
"chrome-extension://nngceckbapebfimnlniiiahkandclblb/",
// Chrome beta extension
"chrome-extension://hccnnhgbibccigepcmlgppchkpfdophk/",
// Edge extension
"chrome-extension://jbkfoedolllekgbhcbcoahefnbanhhlh/",
// Opera extension
"chrome-extension://ccnckbpmaceehanjmeomladnmlffdjgn/",
]);
if (!isDev()) {
return Array.from(ids);
}
// The dev builds of the extension have a different random ID per user, so to make development easier
// we try to find the extension IDs from the user's Chrome profiles when we're running in dev mode.
let chromePaths: string[];
switch (process.platform) {
case "darwin": {
chromePaths = Object.entries(this.getDarwinNMHS())
.filter(([key]) => key !== "Firefox")
.map(([, value]) => value);
break;
}
case "linux": {
chromePaths = Object.entries(this.getLinuxNMHS())
.filter(([key]) => key !== "Firefox")
.map(([, value]) => value);
break;
}
case "win32": {
// TODO: Add more supported browsers for Windows?
chromePaths = [
path.join(process.env.LOCALAPPDATA, "Microsoft", "Edge", "User Data"),
path.join(process.env.LOCALAPPDATA, "Google", "Chrome", "User Data"),
];
break;
}
}
for (const chromePath of chromePaths) {
try {
// The chrome profile directories are named "Default", "Profile 1", "Profile 2", etc.
const profiles = (await fs.readdir(chromePath)).filter((f) => {
const lower = f.toLowerCase();
return lower == "default" || lower.startsWith("profile ");
});
for (const profile of profiles) {
try {
// Read the profile Preferences file and find the extension commands section
const prefs = JSON.parse(
await fs.readFile(path.join(chromePath, profile, "Preferences"), "utf8"),
);
const commands: Map<string, any> = prefs.extensions.commands;
// If one of the commands is autofill_login or generate_password, we know it's probably the Bitwarden extension
for (const { command_name, extension } of Object.values(commands)) {
if (command_name === "autofill_login" || command_name === "generate_password") {
ids.add(`chrome-extension://${extension}/`);
this.logService.info(`Found extension from ${chromePath}: ${extension}`);
}
}
} catch (e) {
this.logService.info(`Error reading preferences: ${e}`);
}
}
} catch (e) {
// Browser is not installed, we can just skip it
}
}
return Array.from(ids);
}
private binaryPath() {
const ext = process.platform === "win32" ? ".exe" : "";
@ -350,52 +421,6 @@ export class NativeMessagingMain {
return path.join(path.dirname(this.exePath), `desktop_proxy${ext}`);
}
private getRegeditInstance() {
// eslint-disable-next-line
const regedit = require("regedit");
regedit.setExternalVBSLocation(path.join(path.dirname(this.exePath), "resources/regedit/vbs"));
return regedit;
}
private async createWindowsRegistry(location: string, jsonFile: string) {
const regedit = this.getRegeditInstance();
const createKey = util.promisify(regedit.createKey);
const putValue = util.promisify(regedit.putValue);
this.logService.debug(`Adding registry: ${location}`);
await createKey(location);
// Insert path to manifest
const obj: any = {};
obj[location] = {
default: {
value: jsonFile,
type: "REG_DEFAULT",
},
};
return putValue(obj);
}
private async deleteWindowsRegistry(key: string) {
const regedit = this.getRegeditInstance();
const list = util.promisify(regedit.list);
const deleteKey = util.promisify(regedit.deleteKey);
this.logService.debug(`Removing registry: ${key}`);
try {
await list(key);
await deleteKey(key);
} catch {
this.logService.error(`Unable to delete registry key: ${key}`);
}
}
private homedir() {
if (process.platform === "darwin") {
return userInfo().homedir;

View File

@ -1,16 +1,16 @@
{
"name": "@bitwarden/desktop",
"version": "2024.10.3",
"version": "2024.11.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@bitwarden/desktop",
"version": "2024.10.3",
"version": "2024.11.0",
"license": "GPL-3.0",
"dependencies": {
"@bitwarden/desktop-napi": "file:../desktop_native/napi",
"argon2": "0.40.1"
"argon2": "0.41.1"
}
},
"../desktop_native/napi": {
@ -35,25 +35,28 @@
}
},
"node_modules/argon2": {
"version": "0.40.1",
"resolved": "https://registry.npmjs.org/argon2/-/argon2-0.40.1.tgz",
"integrity": "sha512-DjtHDwd7pm12qeWyfihHoM8Bn5vGcgH6sKwgPqwNYroRmxlrzadHEvMyuvQxN/V8YSyRRKD5x6ito09q1e9OyA==",
"version": "0.41.1",
"resolved": "https://registry.npmjs.org/argon2/-/argon2-0.41.1.tgz",
"integrity": "sha512-dqCW8kJXke8Ik+McUcMDltrbuAWETPyU6iq+4AhxqKphWi7pChB/Zgd/Tp/o8xRLbg8ksMj46F/vph9wnxpTzQ==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@phc/format": "^1.0.0",
"node-addon-api": "^7.1.0",
"node-gyp-build": "^4.8.0"
"node-addon-api": "^8.1.0",
"node-gyp-build": "^4.8.1"
},
"engines": {
"node": ">=16.17.0"
}
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"license": "MIT"
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.2.1.tgz",
"integrity": "sha512-vmEOvxwiH8tlOcv4SyE8RH34rI5/nWVaigUeAUPawC6f0+HoDthwI0vkMu4tbtsZrXq6QXFfrkhjofzKEs5tpA==",
"license": "MIT",
"engines": {
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/node-gyp-build": {
"version": "4.8.2",

View File

@ -2,7 +2,7 @@
"name": "@bitwarden/desktop",
"productName": "Bitwarden",
"description": "A secure and free password manager for all of your devices.",
"version": "2024.10.3",
"version": "2024.11.0",
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
"homepage": "https://bitwarden.com",
"license": "GPL-3.0",
@ -13,6 +13,6 @@
},
"dependencies": {
"@bitwarden/desktop-napi": "file:../desktop_native/napi",
"argon2": "0.40.1"
"argon2": "0.41.1"
}
}

View File

@ -185,6 +185,7 @@ export class NativeMessageHandlerService {
let decryptedResult = await this.encryptService.decryptToUtf8(
message.encryptedCommand as EncString,
this.ddgSharedSecret,
"ddg-shared-key",
);
decryptedResult = this.trimNullCharsFromMessage(decryptedResult);

View File

@ -114,6 +114,7 @@ export class NativeMessagingService {
await this.encryptService.decryptToUtf8(
rawMessage as EncString,
SymmetricCryptoKey.fromString(await ipc.platform.ephemeralStore.getEphemeralValue(appId)),
`native-messaging-session-${appId}`,
),
);

View File

@ -1,6 +1,6 @@
{
"name": "@bitwarden/web-vault",
"version": "2024.10.5",
"version": "2024.11.0",
"scripts": {
"build:oss": "webpack",
"build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js",

View File

@ -40,9 +40,9 @@
></bit-nav-item>
</bit-nav-group>
<bit-nav-item
*ngIf="isAccessIntelligenceFeatureEnabled"
[text]="'accessIntelligence' | i18n"
route="access-intelligence"
*ngIf="isRiskInsightsFeatureEnabled"
[text]="'riskInsights' | i18n"
route="risk-insights"
></bit-nav-item>
<bit-nav-group
icon="bwi-billing"

View File

@ -51,7 +51,7 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
showPaymentAndHistory$: Observable<boolean>;
hideNewOrgButton$: Observable<boolean>;
organizationIsUnmanaged$: Observable<boolean>;
isAccessIntelligenceFeatureEnabled = false;
isRiskInsightsFeatureEnabled = false;
private _destroy = new Subject<void>();
@ -71,7 +71,7 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
async ngOnInit() {
document.body.classList.remove("layout_frontend");
this.isAccessIntelligenceFeatureEnabled = await this.configService.getFeatureFlag(
this.isRiskInsightsFeatureEnabled = await this.configService.getFeatureFlag(
FeatureFlag.AccessIntelligence,
);

View File

@ -23,15 +23,14 @@
<bit-tab [label]="'role' | i18n">
<ng-container *ngIf="!editMode">
<p bitTypography="body1">{{ "inviteUserDesc" | i18n }}</p>
<bit-form-field *ngIf="remainingSeats$ | async as remainingSeats">
<bit-form-field>
<bit-label>{{ "email" | i18n }}</bit-label>
<input id="emails" type="text" appAutoFocus bitInput formControlName="emails" />
<bit-hint *ngIf="remainingSeats > 1; else singleSeat">{{
"inviteMultipleEmailDesc" | i18n: remainingSeats
<bit-hint>{{
"inviteMultipleEmailDesc"
| i18n
: (organization.productTierType === ProductTierType.TeamsStarter ? "10" : "20")
}}</bit-hint>
<ng-template #singleSeat>
<bit-hint>{{ "inviteSingleEmailDesc" | i18n: remainingSeats }}</bit-hint>
</ng-template>
</bit-form-field>
</ng-container>
<bit-radio-group formControlName="type">
@ -265,6 +264,16 @@
<button
*ngIf="editMode"
type="button"
bitIconButton="bwi-close"
buttonType="danger"
bitFormButton
[appA11yTitle]="'remove' | i18n"
[bitAction]="remove"
[disabled]="loading"
></button>
<button
*ngIf="editMode && params.managedByOrganization === true"
type="button"
bitIconButton="bwi-trash"
buttonType="danger"
bitFormButton

View File

@ -65,6 +65,7 @@ export interface MemberDialogParams {
isOnSecretsManagerStandalone: boolean;
initialTab?: MemberDialogTab;
numConfirmedMembers: number;
managedByOrganization?: boolean;
}
export enum MemberDialogResult {
@ -89,7 +90,6 @@ export class MemberDialogComponent implements OnDestroy {
PermissionMode = PermissionMode;
showNoMasterPasswordWarning = false;
isOnSecretsManagerStandalone: boolean;
remainingSeats$: Observable<number>;
protected organization$: Observable<Organization>;
protected collectionAccessItems: AccessItemView[] = [];
@ -251,10 +251,6 @@ export class MemberDialogComponent implements OnDestroy {
this.loading = false;
});
this.remainingSeats$ = this.organization$.pipe(
map((organization) => organization.seats - this.params.numConfirmedMembers),
);
}
private setFormValidators(organization: Organization) {
@ -469,7 +465,7 @@ export class MemberDialogComponent implements OnDestroy {
this.close(MemberDialogResult.Saved);
};
delete = async () => {
remove = async () => {
if (!this.editMode) {
return;
}
@ -566,6 +562,39 @@ export class MemberDialogComponent implements OnDestroy {
this.close(MemberDialogResult.Restored);
};
delete = async () => {
if (!this.editMode) {
return;
}
const confirmed = await this.dialogService.openSimpleDialog({
title: {
key: "deleteOrganizationUser",
placeholders: [this.params.name],
},
content: { key: "deleteOrganizationUserWarning" },
type: "warning",
acceptButtonText: { key: "delete" },
cancelButtonText: { key: "cancel" },
});
if (!confirmed) {
return false;
}
await this.organizationUserApiService.deleteOrganizationUser(
this.params.organizationId,
this.params.organizationUserId,
);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("organizationUserDeleted", this.params.name),
});
this.close(MemberDialogResult.Deleted);
};
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();

View File

@ -320,6 +320,17 @@
<i aria-hidden="true" class="bwi bwi-close"></i> {{ "remove" | i18n }}
</span>
</button>
<button
*ngIf="u.managedByOrganization === true"
type="button"
bitMenuItem
(click)="deleteUser(u)"
>
<span class="tw-text-danger">
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
{{ "delete" | i18n }}
</span>
</button>
</bit-menu>
</td>
</tr>

View File

@ -486,7 +486,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
const enableUpgradePasswordManagerSub = await firstValueFrom(
this.enableUpgradePasswordManagerSub$,
);
if (enableUpgradePasswordManagerSub) {
if (enableUpgradePasswordManagerSub && this.organization.canEditSubscription) {
const reference = openChangePlanDialog(this.dialogService, {
data: {
organizationId: this.organization.id,
@ -518,6 +518,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
isOnSecretsManagerStandalone: this.orgIsOnSecretsManagerStandalone,
initialTab: initialTab,
numConfirmedMembers: this.dataSource.confirmedUserCount,
managedByOrganization: user?.managedByOrganization,
},
});
@ -725,6 +726,40 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
return true;
}
async deleteUser(user: OrganizationUserView) {
const confirmed = await this.dialogService.openSimpleDialog({
title: {
key: "deleteOrganizationUser",
placeholders: [this.userNamePipe.transform(user)],
},
content: { key: "deleteOrganizationUserWarning" },
type: "warning",
acceptButtonText: { key: "delete" },
cancelButtonText: { key: "cancel" },
});
if (!confirmed) {
return false;
}
this.actionPromise = this.organizationUserApiService.deleteOrganizationUser(
this.organization.id,
user.id,
);
try {
await this.actionPromise;
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("organizationUserDeleted", this.userNamePipe.transform(user)),
});
this.dataSource.removeUser(user);
} catch (e) {
this.validationService.showError(e);
}
this.actionPromise = null;
}
private async noMasterPasswordConfirmationDialog(user: OrganizationUserView) {
return this.dialogService.openSimpleDialog({
title: {

View File

@ -63,10 +63,10 @@ const routes: Routes = [
),
},
{
path: "access-intelligence",
path: "risk-insights",
loadChildren: () =>
import("../../tools/access-intelligence/access-intelligence.module").then(
(m) => m.AccessIntelligenceModule,
import("../../tools/risk-insights/risk-insights.module").then(
(m) => m.RiskInsightsModule,
),
},
{

View File

@ -123,20 +123,22 @@ export class AccountComponent implements OnInit, OnDestroy {
this.canEditSubscription = organization.canEditSubscription;
this.canUseApi = organization.useApi;
// Update disabled states - reactive forms prefers not using disabled attribute
// Disabling these fields for self hosted orgs is deprecated
// This block can be completely removed as part of
// https://bitwarden.atlassian.net/browse/PM-10863
if (!this.limitCollectionCreationDeletionSplitFeatureFlagIsEnabled) {
if (!this.selfHosted) {
this.formGroup.get("orgName").enable();
this.collectionManagementFormGroup.get("limitCollectionCreationDeletion").enable();
this.collectionManagementFormGroup.get("allowAdminAccessToAllCollectionItems").enable();
}
}
if (!this.selfHosted && this.canEditSubscription) {
this.formGroup.get("billingEmail").enable();
// Update disabled states - reactive forms prefers not using disabled attribute
if (!this.selfHosted) {
this.formGroup.get("orgName").enable();
if (this.canEditSubscription) {
this.formGroup.get("billingEmail").enable();
}
}
// Org Response

View File

@ -21,7 +21,13 @@
>
{{ "purgeVault" | i18n }}
</button>
<button type="button" bitButton buttonType="danger" [bitAction]="deleteAccount">
<button
*ngIf="showDeleteAccount$ | async"
type="button"
bitButton
buttonType="danger"
[bitAction]="deleteAccount"
>
{{ "deleteAccount" | i18n }}
</button>
</app-danger-zone>

View File

@ -24,6 +24,7 @@ export class AccountComponent implements OnInit {
showChangeEmail$: Observable<boolean>;
showPurgeVault$: Observable<boolean>;
showDeleteAccount$: Observable<boolean>;
constructor(
private modalService: ModalService,
@ -64,6 +65,16 @@ export class AccountComponent implements OnInit {
!isAccountDeprovisioningEnabled || !userIsManagedByOrganization,
),
);
this.showDeleteAccount$ = combineLatest([
isAccountDeprovisioningEnabled$,
userIsManagedByOrganization$,
]).pipe(
map(
([isAccountDeprovisioningEnabled, userIsManagedByOrganization]) =>
!isAccountDeprovisioningEnabled || !userIsManagedByOrganization,
),
);
}
async deauthorizeSessions() {

View File

@ -1,22 +1,33 @@
import { CommonModule } from "@angular/common";
import { importProvidersFrom } from "@angular/core";
import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { ButtonModule } from "@bitwarden/components";
import { PreloadedEnglishI18nModule } from "../../../core/tests";
import { DangerZoneComponent } from "./danger-zone.component";
class MockConfigService implements Partial<ConfigService> {}
export default {
title: "Web/Danger Zone",
component: DangerZoneComponent,
decorators: [
moduleMetadata({
imports: [ButtonModule, JslibModule],
imports: [ButtonModule, JslibModule, CommonModule],
}),
applicationConfig({
providers: [importProvidersFrom(PreloadedEnglishI18nModule)],
providers: [
importProvidersFrom(PreloadedEnglishI18nModule),
{
provide: ConfigService,
useClass: MockConfigService,
multi: true,
},
],
}),
],
} as Meta;

View File

@ -194,7 +194,7 @@ export class ChangePasswordComponent
HashPurpose.LocalAuthorization,
);
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey, userId);
if (userKey == null) {
this.toastService.showToast({
variant: "error",

View File

@ -48,16 +48,7 @@
}}</span>
</dd>
<dt>{{ "nextCharge" | i18n }}</dt>
<dd *ngIf="!enableTimeThreshold">
{{
nextInvoice
? (nextInvoice.date | date: "mediumDate") +
", " +
(nextInvoice.amount | currency: "$")
: "-"
}}
</dd>
<dd *ngIf="enableTimeThreshold">
<dd>
{{
nextInvoice
? (sub.subscription.periodEndDate | date: "mediumDate") +

View File

@ -38,13 +38,9 @@ export class UserSubscriptionComponent implements OnInit {
sub: SubscriptionResponse;
selfHosted = false;
cloudWebVaultUrl: string;
enableTimeThreshold: boolean;
cancelPromise: Promise<any>;
reinstatePromise: Promise<any>;
protected enableTimeThreshold$ = this.configService.getFeatureFlag$(
FeatureFlag.EnableTimeThreshold,
);
protected deprecateStripeSourcesAPI$ = this.configService.getFeatureFlag$(
FeatureFlag.AC2476_DeprecateStripeSourcesAPI,
@ -69,7 +65,6 @@ export class UserSubscriptionComponent implements OnInit {
async ngOnInit() {
this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$);
await this.load();
this.enableTimeThreshold = await firstValueFrom(this.enableTimeThreshold$);
this.firstLoaded = true;
}

View File

@ -48,10 +48,7 @@
<dt [ngClass]="{ 'tw-text-danger': isExpired }">
{{ "subscriptionExpiration" | i18n }}
</dt>
<dd [ngClass]="{ 'tw-text-danger': isExpired }" *ngIf="!enableTimeThreshold">
{{ nextInvoice ? (nextInvoice.date | date: "mediumDate") : "-" }}
</dd>
<dd [ngClass]="{ 'tw-text-danger': isExpired }" *ngIf="enableTimeThreshold">
<dd [ngClass]="{ 'tw-text-danger': isExpired }">
{{ nextInvoice ? (sub.subscription.periodEndDate | date: "mediumDate") : "-" }}
</dd>
</ng-container>

View File

@ -52,7 +52,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
loading = true;
locale: string;
showUpdatedSubscriptionStatusSection$: Observable<boolean>;
enableTimeThreshold: boolean;
preSelectedProductTier: ProductTierType = ProductTierType.Free;
showSubscription = true;
showSelfHost = false;
@ -65,10 +64,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
FeatureFlag.EnableConsolidatedBilling,
);
protected enableTimeThreshold$ = this.configService.getFeatureFlag$(
FeatureFlag.EnableTimeThreshold,
);
protected enableUpgradePasswordManagerSub$ = this.configService.getFeatureFlag$(
FeatureFlag.EnableUpgradePasswordManagerSub,
);
@ -117,7 +112,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
this.showUpdatedSubscriptionStatusSection$ = this.configService.getFeatureFlag$(
FeatureFlag.AC1795_UpdatedSubscriptionStatusSection,
);
this.enableTimeThreshold = await firstValueFrom(this.enableTimeThreshold$);
}
ngOnDestroy() {
@ -298,9 +292,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
return this.i18nService.t("subscriptionUpgrade", this.sub.seats.toString());
}
} else if (this.sub.maxAutoscaleSeats === this.sub.seats && this.sub.seats != null) {
if (!this.enableTimeThreshold) {
return this.i18nService.t("subscriptionMaxReached", this.sub.seats.toString());
}
const seatAdjustmentMessage = this.sub.plan.isAnnual
? "annualSubscriptionUserSeatsMessage"
: "monthlySubscriptionUserSeatsMessage";
@ -311,21 +302,11 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
} else if (this.userOrg.productTierType === ProductTierType.TeamsStarter) {
return this.i18nService.t("subscriptionUserSeatsWithoutAdditionalSeatsOption", 10);
} else if (this.sub.maxAutoscaleSeats == null) {
if (!this.enableTimeThreshold) {
return this.i18nService.t("subscriptionUserSeatsUnlimitedAutoscale");
}
const seatAdjustmentMessage = this.sub.plan.isAnnual
? "annualSubscriptionUserSeatsMessage"
: "monthlySubscriptionUserSeatsMessage";
return this.i18nService.t(seatAdjustmentMessage);
} else {
if (!this.enableTimeThreshold) {
return this.i18nService.t(
"subscriptionUserSeatsLimitedAutoscale",
this.sub.maxAutoscaleSeats.toString(),
);
}
const seatAdjustmentMessage = this.sub.plan.isAnnual
? "annualSubscriptionUserSeatsMessage"
: "monthlySubscriptionUserSeatsMessage";

View File

@ -1,9 +0,0 @@
import { NgModule } from "@angular/core";
import { AccessIntelligenceRoutingModule } from "./access-intelligence-routing.module";
import { AccessIntelligenceComponent } from "./access-intelligence.component";
@NgModule({
imports: [AccessIntelligenceComponent, AccessIntelligenceRoutingModule],
})
export class AccessIntelligenceModule {}

View File

@ -1,11 +0,0 @@
<!-- <bit-table [dataSource]="dataSource"> -->
<ng-container header>
<tr>
<th bitCell>{{ "application" | i18n }}</th>
<th bitCell>{{ "atRiskPasswords" | i18n }}</th>
<th bitCell>{{ "totalPasswords" | i18n }}</th>
<th bitCell>{{ "atRiskMembers" | i18n }}</th>
<th bitCell>{{ "totalMembers" | i18n }}</th>
</tr>
</ng-container>
<!-- </bit-table> -->

View File

@ -1,19 +0,0 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { TableDataSource, TableModule } from "@bitwarden/components";
@Component({
standalone: true,
selector: "tools-application-table",
templateUrl: "./application-table.component.html",
imports: [CommonModule, JslibModule, TableModule],
})
export class ApplicationTableComponent {
protected dataSource = new TableDataSource<any>();
constructor() {
this.dataSource.data = [];
}
}

View File

@ -1,15 +0,0 @@
<bit-no-items [icon]="noItemsIcon" class="tw-text-main">
<ng-container slot="title">
<h2 class="tw-font-semibold mt-4">
{{ "noPriorityApplicationsTitle" | i18n }}
</h2>
</ng-container>
<ng-container slot="description">
<p class="tw-text-muted">
{{ "noPriorityApplicationsDescription" | i18n }}
</p>
</ng-container>
<ng-container slot="button">
<button bitButton buttonType="primary" type="button">{{ "markPriorityApps" | i18n }}</button>
</ng-container>
</bit-no-items>

View File

@ -1,15 +0,0 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ButtonModule, NoItemsModule, Icons } from "@bitwarden/components";
@Component({
standalone: true,
selector: "tools-no-priority-apps",
templateUrl: "no-priority-apps.component.html",
imports: [ButtonModule, CommonModule, JslibModule, NoItemsModule],
})
export class NoPriorityAppsComponent {
noItemsIcon = Icons.Security;
}

View File

@ -1,123 +0,0 @@
<p>{{ "passwordsReportDesc" | i18n }}</p>
<div *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin tw-text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</div>
<div class="tw-mt-4" *ngIf="!dataSource.data.length">
<tools-no-priority-apps></tools-no-priority-apps>
</div>
<div class="tw-mt-4 tw-flex tw-flex-col" *ngIf="!loading && dataSource.data.length">
<div class="tw-flex tw-gap-6">
<tools-card
class="tw-flex-1"
[title]="'atRiskMembers' | i18n"
[value]="totalMembersMap.size - 3"
[maxValue]="totalMembersMap.size"
>
</tools-card>
<tools-card
class="tw-flex-1"
[title]="'atRiskApplications' | i18n"
[value]="totalMembersMap.size - 1"
[maxValue]="totalMembersMap.size"
>
</tools-card>
</div>
<div class="tw-flex tw-mt-8 tw-mb-4 tw-gap-4">
<bit-search class="tw-grow" [formControl]="searchControl"></bit-search>
<button class="tw-rounded-lg" type="button" buttonType="secondary" bitButton>
<i class="bwi bwi-star-f tw-mr-2"></i>
{{ "markAppAsCritical" | i18n }}
</button>
</div>
<div class="tw-mt-4 tw-flex tw-flex-col" *ngIf="!loading && dataSource.data.length">
<div class="tw-flex tw-gap-6">
<tools-card
class="tw-flex-1"
[title]="'atRiskMembers' | i18n"
[value]="totalMembersMap.size - 3"
[maxValue]="totalMembersMap.size"
>
</tools-card>
<tools-card
class="tw-flex-1"
[title]="'atRiskApplications' | i18n"
[value]="totalMembersMap.size - 1"
[maxValue]="totalMembersMap.size"
>
</tools-card>
</div>
<div class="tw-flex tw-mt-8 tw-mb-4 tw-gap-4">
<bit-search class="tw-grow" [formControl]="searchControl"></bit-search>
<button
class="tw-rounded-lg"
type="button"
buttonType="secondary"
[disabled]="!selectedIds.size"
bitButton
[bitAction]="markAppsAsCritical"
appA11yTitle="{{ 'markAppAsCritical' | i18n }}"
>
<i class="bwi bwi-star-f tw-mr-2"></i>
{{ "markAppAsCritical" | i18n }}
</button>
</div>
<bit-table [dataSource]="dataSource">
<ng-container header>
<tr bitRow>
<th bitCell></th>
<th bitCell bitSortable="name">{{ "name" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "weakness" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "timesReused" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "timesExposed" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "totalMembers" | i18n }}</th>
</tr>
</ng-container>
<ng-template body let-rows$>
<tr bitRow *ngFor="let r of rows$ | async; trackBy: trackByFunction">
<td bitCell>
<input
bitCheckbox
type="checkbox"
[checked]="selectedIds.has(r.id)"
(change)="onCheckboxChange(r.id, $event)"
/>
</td>
<td bitCell>
<ng-container>
<span>{{ r.name }}</span>
</ng-container>
<br />
<small>{{ r.subTitle }}</small>
</td>
<td bitCell class="tw-text-right">
<span
bitBadge
*ngIf="passwordStrengthMap.has(r.id)"
[variant]="passwordStrengthMap.get(r.id)[1]"
>
{{ passwordStrengthMap.get(r.id)[0] | i18n }}
</span>
</td>
<td bitCell class="tw-text-right">
<span bitBadge *ngIf="passwordUseMap.has(r.login.password)" variant="warning">
{{ "reusedXTimes" | i18n: passwordUseMap.get(r.login.password) }}
</span>
</td>
<td bitCell class="tw-text-right">
<span bitBadge *ngIf="exposedPasswordMap.has(r.id)" variant="warning">
{{ "exposedXTimes" | i18n: exposedPasswordMap.get(r.id) }}
</span>
</td>
<td bitCell class="tw-text-right" data-testid="total-membership">
{{ totalMembersMap.get(r.id) || 0 }}
</td>
</tr>
</ng-template>
</bit-table>
</div>
</div>

View File

@ -0,0 +1,113 @@
<div *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin tw-text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</div>
<div class="tw-mt-4" *ngIf="!dataSource.data.length">
<bit-no-items [icon]="noItemsIcon" class="tw-text-main">
<ng-container slot="title">
<h2 class="tw-font-semibold mt-4">
{{ "noAppsInOrgTitle" | i18n: organization.name }}
</h2>
</ng-container>
<ng-container slot="description">
<div class="tw-flex tw-flex-col tw-mb-2">
<span class="tw-text-muted">
{{ "noAppsInOrgDescription" | i18n }}
</span>
<a class="text-primary" routerLink="/login">{{ "learnMore" | i18n }}</a>
</div>
</ng-container>
<ng-container slot="button">
<button (click)="goToCreateNewLoginItem()" bitButton buttonType="primary" type="button">
{{ "createNewLoginItem" | i18n }}
</button>
</ng-container>
</bit-no-items>
</div>
<div class="tw-mt-4 tw-flex tw-flex-col" *ngIf="!loading && dataSource.data.length">
<h2 class="tw-mb-6" bitTypography="h2">{{ "allApplications" | i18n }}</h2>
<div class="tw-flex tw-gap-6">
<tools-card
class="tw-flex-1"
[title]="'atRiskMembers' | i18n"
[value]="mockAtRiskMembersCount"
[maxValue]="mockTotalMembersCount"
>
</tools-card>
<tools-card
class="tw-flex-1"
[title]="'atRiskApplications' | i18n"
[value]="mockAtRiskAppsCount"
[maxValue]="mockTotalAppsCount"
>
</tools-card>
</div>
<div class="tw-flex tw-mt-8 tw-mb-4 tw-gap-4">
<bit-search
[placeholder]="'searchApps' | i18n"
class="tw-grow"
[formControl]="searchControl"
></bit-search>
<button
class="tw-rounded-lg"
type="button"
buttonType="secondary"
bitButton
[disabled]="!selectedIds.size"
[loading]="markingAsCritical"
(click)="markAppsAsCritical()"
>
<i class="bwi bwi-star-f tw-mr-2"></i>
{{ "markAppAsCritical" | i18n }}
</button>
</div>
<bit-table [dataSource]="dataSource">
<ng-container header>
<tr>
<th></th>
<th bitSortable="name" bitCell>{{ "application" | i18n }}</th>
<th bitSortable="atRiskPasswords" bitCell>{{ "atRiskPasswords" | i18n }}</th>
<th bitSortable="totalPasswords" bitCell>{{ "totalPasswords" | i18n }}</th>
<th bitSortable="atRiskMembers" bitCell>{{ "atRiskMembers" | i18n }}</th>
<th bitSortable="totalMembers" bitCell>{{ "totalMembers" | i18n }}</th>
</tr>
</ng-container>
<ng-template body let-rows$>
<tr bitRow *ngFor="let r of rows$ | async; trackBy: trackByFunction">
<td>
<input
bitCheckbox
type="checkbox"
[checked]="selectedIds.has(r.id)"
(change)="onCheckboxChange(r.id, $event)"
/>
</td>
<td bitCell>
<span>{{ r.name }}</span>
</td>
<td bitCell>
<span>
{{ r.atRiskPasswords }}
</span>
</td>
<td bitCell>
<span>
{{ r.totalPasswords }}
</span>
</td>
<td bitCell>
<span>
{{ r.atRiskMembers }}
</span>
</td>
<td bitCell data-testid="total-membership">
{{ r.totalMembers }}
</td>
</tr>
</ng-template>
</bit-table>
</div>

View File

@ -0,0 +1,118 @@
import { Component, DestroyRef, inject, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormControl } from "@angular/forms";
import { ActivatedRoute } from "@angular/router";
import { debounceTime, firstValueFrom, map } from "rxjs";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
Icons,
NoItemsModule,
SearchModule,
TableDataSource,
ToastService,
} from "@bitwarden/components";
import { CardComponent } from "@bitwarden/tools-card";
import { HeaderModule } from "../../layouts/header/header.module";
import { SharedModule } from "../../shared";
import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
import { applicationTableMockData } from "./application-table.mock";
@Component({
standalone: true,
selector: "tools-all-applications",
templateUrl: "./all-applications.component.html",
imports: [HeaderModule, CardComponent, SearchModule, PipesModule, NoItemsModule, SharedModule],
})
export class AllApplicationsComponent implements OnInit {
protected dataSource = new TableDataSource<any>();
protected selectedIds: Set<number> = new Set<number>();
protected searchControl = new FormControl("", { nonNullable: true });
private destroyRef = inject(DestroyRef);
protected loading = false;
protected organization: Organization;
noItemsIcon = Icons.Security;
protected markingAsCritical = false;
// MOCK DATA
protected mockData = applicationTableMockData;
protected mockAtRiskMembersCount = 0;
protected mockAtRiskAppsCount = 0;
protected mockTotalMembersCount = 0;
protected mockTotalAppsCount = 0;
ngOnInit() {
this.activatedRoute.paramMap
.pipe(
takeUntilDestroyed(this.destroyRef),
map(async (params) => {
const organizationId = params.get("organizationId");
this.organization = await firstValueFrom(this.organizationService.get$(organizationId));
// TODO: use organizationId to fetch data
}),
)
.subscribe();
}
constructor(
protected cipherService: CipherService,
protected passwordStrengthService: PasswordStrengthServiceAbstraction,
protected auditService: AuditService,
protected i18nService: I18nService,
protected activatedRoute: ActivatedRoute,
protected toastService: ToastService,
protected organizationService: OrganizationService,
) {
this.dataSource.data = applicationTableMockData;
this.searchControl.valueChanges
.pipe(debounceTime(200), takeUntilDestroyed())
.subscribe((v) => (this.dataSource.filter = v));
}
goToCreateNewLoginItem = async () => {
// TODO: implement
this.toastService.showToast({
variant: "warning",
title: null,
message: "Not yet implemented",
});
};
markAppsAsCritical = async () => {
// TODO: Send to API once implemented
this.markingAsCritical = true;
return new Promise((resolve) => {
setTimeout(() => {
this.selectedIds.clear();
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("appsMarkedAsCritical"),
});
resolve(true);
this.markingAsCritical = false;
}, 1000);
});
};
trackByFunction(_: number, item: CipherView) {
return item.id;
}
onCheckboxChange(id: number, event: Event) {
const isChecked = (event.target as HTMLInputElement).checked;
if (isChecked) {
this.selectedIds.add(id);
} else {
this.selectedIds.delete(id);
}
}
}

View File

@ -0,0 +1,50 @@
export const applicationTableMockData = [
{
id: 1,
name: "google.com",
atRiskPasswords: 4,
totalPasswords: 10,
atRiskMembers: 2,
totalMembers: 5,
},
{
id: 2,
name: "facebook.com",
atRiskPasswords: 3,
totalPasswords: 8,
atRiskMembers: 1,
totalMembers: 3,
},
{
id: 3,
name: "twitter.com",
atRiskPasswords: 2,
totalPasswords: 6,
atRiskMembers: 0,
totalMembers: 2,
},
{
id: 4,
name: "linkedin.com",
atRiskPasswords: 1,
totalPasswords: 4,
atRiskMembers: 0,
totalMembers: 1,
},
{
id: 5,
name: "instagram.com",
atRiskPasswords: 0,
totalPasswords: 2,
atRiskMembers: 0,
totalMembers: 0,
},
{
id: 6,
name: "tiktok.com",
atRiskPasswords: 0,
totalPasswords: 1,
atRiskMembers: 0,
totalMembers: 0,
},
];

View File

@ -0,0 +1,99 @@
<div *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin tw-text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</div>
<div class="tw-mt-4" *ngIf="!dataSource.data.length">
<bit-no-items [icon]="noItemsIcon" class="tw-text-main">
<ng-container slot="title">
<h2 class="tw-font-semibold mt-4">
{{ "noCriticalAppsTitle" | i18n }}
</h2>
</ng-container>
<ng-container slot="description">
<p class="tw-text-muted">
{{ "noCriticalAppsDescription" | i18n }}
</p>
</ng-container>
<ng-container slot="button">
<button (click)="goToAllAppsTab()" bitButton buttonType="primary" type="button">
{{ "markCriticalApps" | i18n }}
</button>
</ng-container>
</bit-no-items>
</div>
<div class="tw-mt-4 tw-flex tw-flex-col" *ngIf="!loading && dataSource.data.length">
<div class="tw-flex tw-justify-between tw-mb-4">
<h2 bitTypography="h2">{{ "criticalApplications" | i18n }}</h2>
<button bitButton buttonType="primary" type="button">
<i class="bwi bwi-envelope tw-mr-2"></i>
{{ "requestPasswordChange" | i18n }}
</button>
</div>
<div class="tw-flex tw-gap-6">
<tools-card
class="tw-flex-1"
[title]="'atRiskMembers' | i18n"
[value]="mockAtRiskMembersCount"
[maxValue]="mockTotalMembersCount"
>
</tools-card>
<tools-card
class="tw-flex-1"
[title]="'atRiskApplications' | i18n"
[value]="mockAtRiskAppsCount"
[maxValue]="mockTotalAppsCount"
>
</tools-card>
</div>
<div class="tw-flex tw-mt-8 tw-mb-4 tw-gap-4">
<bit-search
[placeholder]="'searchApps' | i18n"
class="tw-grow"
[formControl]="searchControl"
></bit-search>
</div>
<bit-table [dataSource]="dataSource">
<ng-container header>
<tr>
<th></th>
<th bitSortable="name" bitCell>{{ "application" | i18n }}</th>
<th bitSortable="atRiskPasswords" bitCell>{{ "atRiskPasswords" | i18n }}</th>
<th bitSortable="totalPasswords" bitCell>{{ "totalPasswords" | i18n }}</th>
<th bitSortable="atRiskMembers" bitCell>{{ "atRiskMembers" | i18n }}</th>
<th bitSortable="totalMembers" bitCell>{{ "totalMembers" | i18n }}</th>
</tr>
</ng-container>
<ng-template body let-rows$>
<tr bitRow *ngFor="let r of rows$ | async">
<td>
<i class="bwi bwi-star-f"></i>
</td>
<td bitCell>
<span>{{ r.name }}</span>
</td>
<td bitCell>
<span>
{{ r.atRiskPasswords }}
</span>
</td>
<td bitCell>
<span>
{{ r.totalPasswords }}
</span>
</td>
<td bitCell>
<span>
{{ r.atRiskMembers }}
</span>
</td>
<td bitCell data-testid="total-membership">
{{ r.totalMembers }}
</td>
</tr>
</ng-template>
</bit-table>
</div>

View File

@ -0,0 +1,68 @@
import { Component, DestroyRef, inject, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormControl } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { debounceTime, map } from "rxjs";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { SearchModule, TableDataSource, NoItemsModule, Icons } from "@bitwarden/components";
import { CardComponent } from "@bitwarden/tools-card";
import { HeaderModule } from "../../layouts/header/header.module";
import { SharedModule } from "../../shared";
import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
import { applicationTableMockData } from "./application-table.mock";
import { RiskInsightsTabType } from "./risk-insights.component";
@Component({
standalone: true,
selector: "tools-critical-applications",
templateUrl: "./critical-applications.component.html",
imports: [CardComponent, HeaderModule, SearchModule, NoItemsModule, PipesModule, SharedModule],
})
export class CriticalApplicationsComponent implements OnInit {
protected dataSource = new TableDataSource<any>();
protected selectedIds: Set<number> = new Set<number>();
protected searchControl = new FormControl("", { nonNullable: true });
private destroyRef = inject(DestroyRef);
protected loading = false;
protected organizationId: string;
noItemsIcon = Icons.Security;
// MOCK DATA
protected mockData = applicationTableMockData;
protected mockAtRiskMembersCount = 0;
protected mockAtRiskAppsCount = 0;
protected mockTotalMembersCount = 0;
protected mockTotalAppsCount = 0;
ngOnInit() {
this.activatedRoute.paramMap
.pipe(
takeUntilDestroyed(this.destroyRef),
map(async (params) => {
this.organizationId = params.get("organizationId");
// TODO: use organizationId to fetch data
}),
)
.subscribe();
}
goToAllAppsTab = async () => {
await this.router.navigate([`organizations/${this.organizationId}/risk-insights`], {
queryParams: { tabIndex: RiskInsightsTabType.AllApps },
queryParamsHandling: "merge",
});
};
constructor(
protected i18nService: I18nService,
protected activatedRoute: ActivatedRoute,
protected router: Router,
) {
this.dataSource.data = []; //applicationTableMockData;
this.searchControl.valueChanges
.pipe(debounceTime(200), takeUntilDestroyed())
.subscribe((v) => (this.dataSource.filter = v));
}
}

View File

@ -4,7 +4,7 @@ import { mock, MockProxy } from "jest-mock-extended";
import { of } from "rxjs";
// eslint-disable-next-line no-restricted-imports
import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/access-intelligence";
import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/risk-insights";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";

View File

@ -6,7 +6,7 @@ import { map } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
// eslint-disable-next-line no-restricted-imports
import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/access-intelligence";
import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/risk-insights";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";

View File

@ -0,0 +1,64 @@
<p>{{ "passwordsReportDesc" | i18n }}</p>
<div *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin tw-text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</div>
<div class="tw-flex tw-flex-col" *ngIf="!loading && dataSource.data.length">
<bit-table [dataSource]="dataSource">
<ng-container header>
<tr bitRow>
<th bitCell></th>
<th bitCell bitSortable="name">{{ "name" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "weakness" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "timesReused" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "timesExposed" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "totalMembers" | i18n }}</th>
</tr>
</ng-container>
<ng-template body let-rows$>
<tr bitRow *ngFor="let r of rows$ | async; trackBy: trackByFunction">
<td bitCell>
<input
bitCheckbox
type="checkbox"
[checked]="selectedIds.has(r.id)"
(change)="onCheckboxChange(r.id, $event)"
/>
</td>
<td bitCell>
<ng-container>
<span>{{ r.name }}</span>
</ng-container>
<br />
<small>{{ r.subTitle }}</small>
</td>
<td bitCell class="tw-text-right">
<span
bitBadge
*ngIf="passwordStrengthMap.has(r.id)"
[variant]="passwordStrengthMap.get(r.id)[1]"
>
{{ passwordStrengthMap.get(r.id)[0] | i18n }}
</span>
</td>
<td bitCell class="tw-text-right">
<span bitBadge *ngIf="passwordUseMap.has(r.login.password)" variant="warning">
{{ "reusedXTimes" | i18n: passwordUseMap.get(r.login.password) }}
</span>
</td>
<td bitCell class="tw-text-right">
<span bitBadge *ngIf="exposedPasswordMap.has(r.id)" variant="warning">
{{ "exposedXTimes" | i18n: exposedPasswordMap.get(r.id) }}
</span>
</td>
<td bitCell class="tw-text-right" data-testid="total-membership">
{{ totalMembersMap.get(r.id) || 0 }}
</td>
</tr>
</ng-template>
</bit-table>
</div>

View File

@ -5,7 +5,7 @@ import { ActivatedRoute } from "@angular/router";
import { debounceTime, map } from "rxjs";
// eslint-disable-next-line no-restricted-imports
import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/access-intelligence";
import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/risk-insights";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
@ -27,8 +27,6 @@ import { OrganizationBadgeModule } from "../../vault/individual-vault/organizati
// eslint-disable-next-line no-restricted-imports
import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
import { NoPriorityAppsComponent } from "./no-priority-apps.component";
@Component({
standalone: true,
selector: "tools-password-health-members",
@ -40,7 +38,6 @@ import { NoPriorityAppsComponent } from "./no-priority-apps.component";
HeaderModule,
SearchModule,
FormsModule,
NoPriorityAppsComponent,
SharedModule,
TableModule,
],
@ -100,7 +97,7 @@ export class PasswordHealthMembersComponent implements OnInit {
await passwordHealthService.generateReport();
this.dataSource.data = []; //passwordHealthService.reportCiphers;
this.dataSource.data = passwordHealthService.reportCiphers;
this.exposedPasswordMap = passwordHealthService.exposedPasswordMap;
this.passwordStrengthMap = passwordHealthService.passwordStrengthMap;

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