1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-01-31 22:51:28 +01:00

Merge branch 'main' into pm-13345-Add-Remove-Bitwarden-Families-policy-in-Admin-Console

This commit is contained in:
Cy Okeke 2024-11-08 10:26:51 +01:00
commit cf81e6af1a
No known key found for this signature in database
GPG Key ID: 88B341B55C84B45C
156 changed files with 2330 additions and 1128 deletions

50
.github/renovate.json vendored
View File

@ -41,16 +41,12 @@
},
{
"matchPackageNames": [
"@ngtools/webpack",
"base64-loader",
"buffer",
"bufferutil",
"copy-webpack-plugin",
"core-js",
"css-loader",
"html-loader",
"html-webpack-injector",
"html-webpack-plugin",
"mini-css-extract-plugin",
"ngx-infinite-scroll",
"postcss",
@ -60,20 +56,15 @@
"sass-loader",
"style-loader",
"ts-loader",
"tsconfig-paths-webpack-plugin",
"url",
"util",
"webpack",
"webpack-cli",
"webpack-dev-server",
"webpack-node-externals"
"util"
],
"description": "Admin Console owned dependencies",
"commitMessagePrefix": "[deps] AC:",
"reviewers": ["team:team-admin-console-dev"]
},
{
"matchPackageNames": ["@types/node-ipc", "node-ipc", "qrious"],
"matchPackageNames": ["qrious"],
"description": "Auth owned dependencies",
"commitMessagePrefix": "[deps] Auth:",
"reviewers": ["team:team-auth-dev"]
@ -110,27 +101,43 @@
},
{
"matchPackageNames": [
"@babel/core",
"@babel/preset-env",
"@electron/notarize",
"@electron/rebuild",
"@types/argon2-browser",
"@ngtools/webpack",
"@types/chrome",
"@types/firefox-webext-browser",
"@types/glob",
"@types/jquery",
"@types/lowdb",
"@types/node",
"@types/node-forge",
"argon2",
"argon2-browser",
"big-integer",
"@types/node-ipc",
"@yao-pkg",
"babel-loader",
"browserslist",
"copy-webpack-plugin",
"electron",
"electron-builder",
"electron-log",
"electron-reload",
"electron-store",
"electron-updater",
"electron",
"html-webpack-injector",
"html-webpack-plugin",
"lowdb",
"node-forge",
"node-ipc",
"pkg",
"rxjs",
"tsconfig-paths-webpack-plugin",
"type-fest",
"typescript"
"typescript",
"webpack",
"webpack-cli",
"webpack-dev-server",
"webpack-node-externals"
],
"description": "Platform owned dependencies",
"commitMessagePrefix": "[deps] Platform:",
@ -231,7 +238,6 @@
"@types/koa__router",
"@types/koa-bodyparser",
"@types/koa-json",
"@types/lowdb",
"@types/lunr",
"@types/node-fetch",
"@types/proper-lockfile",
@ -244,18 +250,22 @@
"koa",
"koa-bodyparser",
"koa-json",
"lowdb",
"lunr",
"multer",
"node-fetch",
"open",
"pkg",
"proper-lockfile",
"qrcode-parser"
],
"description": "Vault owned dependencies",
"commitMessagePrefix": "[deps] Vault:",
"reviewers": ["team:team-vault-dev"]
},
{
"matchPackageNames": ["@types/argon2-browser", "argon2", "argon2-browser", "big-integer"],
"description": "Key Management owned dependencies",
"commitMessagePrefix": "[deps] KM:",
"reviewers": ["team:team-key-management-dev"]
}
],
"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
@ -225,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
@ -342,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
@ -381,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

@ -2878,7 +2878,7 @@
"message": "E-Mail generieren"
},
"generatorBoundariesHint": {
"message": "Value must be between $MIN$ and $MAX$",
"message": "Wert muss zwischen $MIN$ und $MAX$ liegen",
"description": "Explains spin box minimum and maximum values to the user",
"placeholders": {
"min": {

View File

@ -20,16 +20,16 @@
"message": "Crear cuenta"
},
"newToBitwarden": {
"message": "New to Bitwarden?"
"message": "¿Nuevo en Bitwarden?"
},
"logInWithPasskey": {
"message": "Log in with passkey"
"message": "Iniciar sesión con clave de acceso"
},
"useSingleSignOn": {
"message": "Use single sign-on"
"message": "Usar inicio de sesión único"
},
"welcomeBack": {
"message": "Welcome back"
"message": "Bienvenido de nuevo"
},
"setAStrongPassword": {
"message": "Establece una contraseña fuerte"
@ -84,7 +84,7 @@
"message": "Incorporarse a la organización"
},
"joinOrganizationName": {
"message": "Join $ORGANIZATIONNAME$",
"message": "Unirse a $ORGANIZATIONNAME$",
"placeholders": {
"organizationName": {
"content": "$1",
@ -120,7 +120,7 @@
"message": "Copiar contraseña"
},
"copyPassphrase": {
"message": "Copy passphrase"
"message": "Copiar frase de contraseña"
},
"copyNote": {
"message": "Copiar nota"
@ -153,7 +153,7 @@
"message": "Copiar número de licencia"
},
"copyCustomField": {
"message": "Copy $FIELD$",
"message": "Copiar $FIELD$",
"placeholders": {
"field": {
"content": "$1",
@ -162,13 +162,13 @@
}
},
"copyWebsite": {
"message": "Copy website"
"message": "Copiar sitio web"
},
"copyNotes": {
"message": "Copy notes"
"message": "Copiar notas"
},
"fill": {
"message": "Fill",
"message": "Rellenar",
"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": {
@ -223,13 +223,13 @@
"message": "Añadir elemento"
},
"accountEmail": {
"message": "Account email"
"message": "Correo electrónico de la cuenta"
},
"requestHint": {
"message": "Request hint"
"message": "Solicitar pista"
},
"requestPasswordHint": {
"message": "Request password hint"
"message": "Solicitar pista de la contraseña"
},
"enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": {
"message": "Enter your account email address and your password hint will be sent to you"
@ -427,7 +427,7 @@
"message": "Generar contraseña"
},
"generatePassphrase": {
"message": "Generate passphrase"
"message": "Generar frase de contraseña"
},
"regeneratePassword": {
"message": "Regenerar contraseña"
@ -567,7 +567,7 @@
"message": "Notas"
},
"privateNote": {
"message": "Private note"
"message": "Nota privada"
},
"note": {
"message": "Nota"
@ -624,7 +624,7 @@
"message": "Tiempo de sesión agotado"
},
"vaultTimeoutHeader": {
"message": "Vault timeout"
"message": "Tiempo de espera de la caja fuerte"
},
"otherOptions": {
"message": "Otras opciones"
@ -645,13 +645,13 @@
"message": "Tu caja fuerte está bloqueada. Verifica tu identidad para continuar."
},
"yourVaultIsLockedV2": {
"message": "Your vault is locked"
"message": "Tu caja fuerte está bloqueada"
},
"yourAccountIsLocked": {
"message": "Your account is locked"
"message": "Tu cuenta está bloqueada"
},
"or": {
"message": "or"
"message": "o"
},
"unlock": {
"message": "Desbloquear"
@ -676,7 +676,7 @@
"message": "Tiempo de espera de la caja fuerte"
},
"vaultTimeout1": {
"message": "Timeout"
"message": "Tiempo de espera"
},
"lockNow": {
"message": "Bloquear"
@ -4708,11 +4708,11 @@
"description": "Represents the - key in screen reader content as a readable word"
},
"plusCharacterDescriptor": {
"message": "Plus",
"message": "s",
"description": "Represents the + key in screen reader content as a readable word"
},
"equalsCharacterDescriptor": {
"message": "Equals",
"message": "Igual",
"description": "Represents the = key in screen reader content as a readable word"
},
"braceLeftCharacterDescriptor": {
@ -4736,15 +4736,15 @@
"description": "Represents the | key in screen reader content as a readable word"
},
"backSlashCharacterDescriptor": {
"message": "Back slash",
"message": "Contrabarra",
"description": "Represents the back slash key in screen reader content as a readable word"
},
"colonCharacterDescriptor": {
"message": "Colon",
"message": "Dos puntos",
"description": "Represents the : key in screen reader content as a readable word"
},
"semicolonCharacterDescriptor": {
"message": "Semicolon",
"message": "Punto y coma",
"description": "Represents the ; key in screen reader content as a readable word"
},
"doubleQuoteCharacterDescriptor": {

View File

@ -1424,7 +1424,7 @@
"message": "Palvelimen URL"
},
"selfHostBaseUrl": {
"message": "Self-host server URL",
"message": "Itse ylläpidetyn palvelimen URL-osoite",
"description": "Label for field requesting a self-hosted integration service URL"
},
"apiUrl": {
@ -1795,13 +1795,13 @@
"message": "Salasanahistoria"
},
"generatorHistory": {
"message": "Generator history"
"message": "Generaattorihistoria"
},
"clearGeneratorHistoryTitle": {
"message": "Clear generator history"
"message": "Tyhjennä generaattorihistoria"
},
"cleargGeneratorHistoryDescription": {
"message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?"
"message": "Jos jatkat, kaikki generaattorihistorian kohteet poistetaan. Haluatko varmasti jatkaa?"
},
"back": {
"message": "Takaisin"
@ -1920,7 +1920,7 @@
"message": "Tyhjennä historia"
},
"nothingToShow": {
"message": "Nothing to show"
"message": "Mitään näytettävää ei ole"
},
"nothingGeneratedRecently": {
"message": "Et ole luonut mitään hiljattain"
@ -2710,7 +2710,7 @@
"description": "Used as a card title description on the set password page to explain why the user is there"
},
"cardMetrics": {
"message": "out of $TOTAL$",
"message": "/$TOTAL$",
"placeholders": {
"total": {
"content": "$1",

View File

@ -147,7 +147,7 @@
"message": "Kopiera personnummer"
},
"copyPassportNumber": {
"message": "Copy passport number"
"message": "Kopiera passnummer"
},
"copyLicenseNumber": {
"message": "Copy license number"
@ -4624,7 +4624,7 @@
"message": "Items that have been in trash more than 30 days will automatically be deleted"
},
"restore": {
"message": "Restore"
"message": "Återställ"
},
"deleteForever": {
"message": "Delete forever"
@ -4744,7 +4744,7 @@
"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,11 +4756,11 @@
"description": "Represents the ' key in screen reader content as a readable word"
},
"lessThanCharacterDescriptor": {
"message": "Less than",
"message": "Mindre än",
"description": "Represents the < key in screen reader content as a readable word"
},
"greaterThanCharacterDescriptor": {
"message": "Greater than",
"message": "Större än",
"description": "Represents the > key in screen reader content as a readable word"
},
"commaCharacterDescriptor": {
@ -4772,7 +4772,7 @@
"description": "Represents the . key in screen reader content as a readable word"
},
"questionCharacterDescriptor": {
"message": "Question mark",
"message": "Frågetecken",
"description": "Represents the ? key in screen reader content as a readable word"
},
"forwardSlashCharacterDescriptor": {

View File

@ -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": {
@ -458,7 +458,7 @@
"description": "deprecated. Use specialCharactersLabel instead."
},
"include": {
"message": "Include",
"message": "包含",
"description": "Card header for password generator include block"
},
"uppercaseDescription": {
@ -730,10 +730,10 @@
"message": "安全"
},
"confirmMasterPassword": {
"message": "Confirm master password"
"message": "確認主密碼"
},
"masterPassword": {
"message": "Master password"
"message": "主密碼"
},
"masterPassImportant": {
"message": "Your master password cannot be recovered if you forget it!"
@ -1092,10 +1092,10 @@
"message": "This file export will be password protected and require the file password to decrypt."
},
"filePassword": {
"message": "File password"
"message": "檔案密碼"
},
"exportPasswordDescription": {
"message": "This password will be used to export and import this file"
"message": "此密碼將用於匯出和匯入此檔案"
},
"accountRestrictedOptionDescription": {
"message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account."
@ -3542,7 +3542,7 @@
"description": "Screen reader text (aria-label) for new item button in overlay"
},
"newLogin": {
"message": "New login",
"message": "新增登入資訊",
"description": "Button text to display within inline menu when there are no matching items on a login field"
},
"addNewLoginItemAria": {

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

@ -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

@ -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,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

@ -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"
@ -388,9 +388,9 @@ dependencies = [
[[package]]
name = "core-foundation"
version = "0.9.4"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63"
dependencies = [
"core-foundation-sys",
"libc",
@ -1154,9 +1154,9 @@ dependencies = [
[[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",
@ -1667,9 +1667,9 @@ checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152"
[[package]]
name = "security-framework"
version = "2.11.0"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0"
checksum = "f9d0283c0a4a22a0f1b0e4edca251aa20b92fc96eaa09b84bec052f9415e9d71"
dependencies = [
"bitflags",
"core-foundation",
@ -1680,9 +1680,9 @@ dependencies = [
[[package]]
name = "security-framework-sys"
version = "2.11.0"
version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7"
checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6"
dependencies = [
"core-foundation-sys",
"libc",
@ -1867,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",
@ -2487,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",
@ -2525,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",
@ -2583,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",
@ -2596,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",
@ -2609,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,7 +38,7 @@ rand = "=0.8.5"
retry = "=2.0.0"
scopeguard = "=1.2.0"
sha2 = "=0.10.8"
thiserror = "=1.0.61"
thiserror = "=1.0.68"
tokio = { version = "=1.41.0", features = ["io-util", "sync", "macros"] }
tokio-util = "=0.7.12"
typenum = "=1.17.0"
@ -61,12 +61,12 @@ windows = { version = "=0.57.0", features = [
keytar = "=0.1.6"
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation = { version = "=0.9.4", optional = true }
security-framework = { version = "=2.11.0", optional = true }
security-framework-sys = { version = "=2.11.0", optional = true }
core-foundation = { version = "=0.10.0", optional = true }
security-framework = { version = "=3.0.0", optional = true }
security-framework-sys = { version = "=2.12.0", optional = true }
[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

@ -14,9 +14,9 @@ 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"

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

@ -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

@ -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

@ -1684,10 +1684,10 @@
"message": "Die Kontolöschung ist dauerhaft. Sie kann nicht rückgängig gemacht werden."
},
"cannotDeleteAccount": {
"message": "Cannot delete account"
"message": "Konto kann nicht gelöscht werden"
},
"cannotDeleteAccountDesc": {
"message": "This action cannot be completed because your account is owned by an organization. Contact your organization administrator for additional details."
"message": "Diese Aktion kann nicht abgeschlossen werden, da dein Konto im Besitz einer Organisation ist. Kontaktiere deinen Organisationsadministrator für weitere Details."
},
"accountDeleted": {
"message": "Konto gelöscht"
@ -2394,7 +2394,7 @@
"message": "E-Mail generieren"
},
"generatorBoundariesHint": {
"message": "Value must be between $MIN$ and $MAX$",
"message": "Wert muss zwischen $MIN$ und $MAX$ liegen",
"description": "Explains spin box minimum and maximum values to the user",
"placeholders": {
"min": {

View File

@ -1800,7 +1800,7 @@
"description": "Used as a card title description on the set password page to explain why the user is there"
},
"cardMetrics": {
"message": "out of $TOTAL$",
"message": "/$TOTAL$",
"placeholders": {
"total": {
"content": "$1",

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

@ -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

@ -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,120 +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 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

@ -57,6 +57,7 @@
type="button"
buttonType="secondary"
bitButton
*ngIf="isCritialAppsFeatureEnabled"
[disabled]="!selectedIds.size"
[loading]="markingAsCritical"
(click)="markAppsAsCritical()"
@ -68,7 +69,7 @@
<bit-table [dataSource]="dataSource">
<ng-container header>
<tr>
<th></th>
<th *ngIf="isCritialAppsFeatureEnabled"></th>
<th bitSortable="name" bitCell>{{ "application" | i18n }}</th>
<th bitSortable="atRiskPasswords" bitCell>{{ "atRiskPasswords" | i18n }}</th>
<th bitSortable="totalPasswords" bitCell>{{ "totalPasswords" | i18n }}</th>
@ -78,7 +79,7 @@
</ng-container>
<ng-template body let-rows$>
<tr bitRow *ngFor="let r of rows$ | async; trackBy: trackByFunction">
<td>
<td *ngIf="isCritialAppsFeatureEnabled">
<input
bitCheckbox
type="checkbox"

View File

@ -7,6 +7,8 @@ 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 { 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 { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@ -41,6 +43,7 @@ export class AllApplicationsComponent implements OnInit {
protected organization: Organization;
noItemsIcon = Icons.Security;
protected markingAsCritical = false;
isCritialAppsFeatureEnabled = false;
// MOCK DATA
protected mockData = applicationTableMockData;
@ -49,7 +52,7 @@ export class AllApplicationsComponent implements OnInit {
protected mockTotalMembersCount = 0;
protected mockTotalAppsCount = 0;
ngOnInit() {
async ngOnInit() {
this.activatedRoute.paramMap
.pipe(
takeUntilDestroyed(this.destroyRef),
@ -60,6 +63,10 @@ export class AllApplicationsComponent implements OnInit {
}),
)
.subscribe();
this.isCritialAppsFeatureEnabled = await this.configService.getFeatureFlag(
FeatureFlag.CriticalApps,
);
}
constructor(
@ -70,6 +77,7 @@ export class AllApplicationsComponent implements OnInit {
protected activatedRoute: ActivatedRoute,
protected toastService: ToastService,
protected organizationService: OrganizationService,
protected configService: ConfigService,
) {
this.dataSource.data = applicationTableMockData;
this.searchControl.valueChanges

View File

@ -12,8 +12,8 @@ import { HeaderModule } from "../../layouts/header/header.module";
import { SharedModule } from "../../shared";
import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
import { AccessIntelligenceTabType } from "./access-intelligence.component";
import { applicationTableMockData } from "./application-table.mock";
import { RiskInsightsTabType } from "./risk-insights.component";
@Component({
standalone: true,
@ -49,8 +49,8 @@ export class CriticalApplicationsComponent implements OnInit {
}
goToAllAppsTab = async () => {
await this.router.navigate([`organizations/${this.organizationId}/access-intelligence`], {
queryParams: { tabIndex: AccessIntelligenceTabType.AllApps },
await this.router.navigate([`organizations/${this.organizationId}/risk-insights`], {
queryParams: { tabIndex: RiskInsightsTabType.AllApps },
queryParamsHandling: "merge",
});
};

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";

View File

@ -4,7 +4,7 @@ import { mock } 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";

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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";

View File

@ -4,15 +4,15 @@ import { RouterModule, Routes } from "@angular/router";
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { AccessIntelligenceComponent } from "./access-intelligence.component";
import { RiskInsightsComponent } from "./risk-insights.component";
const routes: Routes = [
{
path: "",
component: AccessIntelligenceComponent,
component: RiskInsightsComponent,
canActivate: [canAccessFeature(FeatureFlag.AccessIntelligence)],
data: {
titleId: "accessIntelligence",
titleId: "RiskInsights",
},
},
];
@ -21,4 +21,4 @@ const routes: Routes = [
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class AccessIntelligenceRoutingModule {}
export class RiskInsightsRoutingModule {}

View File

@ -1,4 +1,4 @@
<div class="tw-mb-1 text-primary" bitTypography="body1">{{ "accessIntelligence" | i18n }}</div>
<div class="tw-mb-1 text-primary" bitTypography="body1">{{ "riskInsights" | i18n }}</div>
<h1 bitTypography="h1">{{ "passwordRisk" | i18n }}</h1>
<div class="tw-text-muted">{{ "discoverAtRiskPasswords" | i18n }}</div>
<div class="tw-bg-primary-100 tw-rounded-lg tw-w-full tw-px-8 tw-py-2 tw-my-4">
@ -19,7 +19,7 @@
<bit-tab label="{{ 'allApplicationsWithCount' | i18n: apps.length }}">
<tools-all-applications></tools-all-applications>
</bit-tab>
<bit-tab>
<bit-tab *ngIf="isCritialAppsFeatureEnabled">
<ng-template bitTabLabel>
<i class="bwi bwi-star"></i>
{{ "criticalApplicationsWithCount" | i18n: criticalApps.length }}

View File

@ -1,9 +1,11 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { Component, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute, Router } from "@angular/router";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { AsyncActionsModule, ButtonModule, TabsModule } from "@bitwarden/components";
import { HeaderModule } from "../../layouts/header/header.module";
@ -15,7 +17,7 @@ import { PasswordHealthMembersURIComponent } from "./password-health-members-uri
import { PasswordHealthMembersComponent } from "./password-health-members.component";
import { PasswordHealthComponent } from "./password-health.component";
export enum AccessIntelligenceTabType {
export enum RiskInsightsTabType {
AllApps = 0,
CriticalApps = 1,
NotifiedMembers = 2,
@ -23,7 +25,7 @@ export enum AccessIntelligenceTabType {
@Component({
standalone: true,
templateUrl: "./access-intelligence.component.html",
templateUrl: "./risk-insights.component.html",
imports: [
AllApplicationsComponent,
AsyncActionsModule,
@ -39,9 +41,10 @@ export enum AccessIntelligenceTabType {
TabsModule,
],
})
export class AccessIntelligenceComponent {
tabIndex: AccessIntelligenceTabType;
export class RiskInsightsComponent implements OnInit {
tabIndex: RiskInsightsTabType;
dataLastUpdated = new Date();
isCritialAppsFeatureEnabled = false;
apps: any[] = [];
criticalApps: any[] = [];
@ -65,12 +68,19 @@ export class AccessIntelligenceComponent {
});
};
async ngOnInit() {
this.isCritialAppsFeatureEnabled = await this.configService.getFeatureFlag(
FeatureFlag.CriticalApps,
);
}
constructor(
protected route: ActivatedRoute,
private router: Router,
private configService: ConfigService,
) {
route.queryParams.pipe(takeUntilDestroyed()).subscribe(({ tabIndex }) => {
this.tabIndex = !isNaN(tabIndex) ? tabIndex : AccessIntelligenceTabType.AllApps;
this.tabIndex = !isNaN(tabIndex) ? tabIndex : RiskInsightsTabType.AllApps;
});
}
}

View File

@ -0,0 +1,9 @@
import { NgModule } from "@angular/core";
import { RiskInsightsRoutingModule } from "./risk-insights-routing.module";
import { RiskInsightsComponent } from "./risk-insights.component";
@NgModule({
imports: [RiskInsightsComponent, RiskInsightsRoutingModule],
})
export class RiskInsightsModule {}

View File

@ -7,6 +7,7 @@
*ngIf="showCipherView"
[cipher]="cipher"
[collections]="collections"
[isAdminConsole]="formConfig.isAdminConsole"
></app-cipher-view>
<vault-cipher-form
*ngIf="loadForm"

View File

@ -6,6 +6,7 @@ import { firstValueFrom, Observable, Subject } from "rxjs";
import { map } from "rxjs/operators";
import { CollectionView } from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
@ -17,6 +18,8 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import {
@ -231,6 +234,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
private billingAccountProfileStateService: BillingAccountProfileStateService,
private premiumUpgradeService: PremiumUpgradePromptService,
private cipherAuthorizationService: CipherAuthorizationService,
private apiService: ApiService,
) {
this.updateTitle();
}
@ -278,7 +282,20 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
if (this._originalFormMode === "add" || this._originalFormMode === "clone") {
this.formConfig.mode = "edit";
}
this.formConfig.originalCipher = await this.cipherService.get(cipherView.id);
let cipher: Cipher;
// When the form config is used within the Admin Console, retrieve the cipher from the admin endpoint
if (this.formConfig.isAdminConsole) {
const cipherResponse = await this.apiService.getCipherAdmin(cipherView.id);
const cipherData = new CipherData(cipherResponse);
cipher = new Cipher(cipherData);
} else {
cipher = await this.cipherService.get(cipherView.id);
}
// Store the updated cipher so any following edits use the most up to date cipher
this.formConfig.originalCipher = cipher;
this._cipherModified = true;
await this.changeMode("view");
}

View File

@ -16,13 +16,39 @@
"all" | i18n
}}</label>
</th>
<th bitCell [class]="showExtraColumn ? 'lg:tw-w-3/5' : 'tw-w-full'">{{ "name" | i18n }}</th>
<!-- Organization vault -->
<th
*ngIf="showAdminActions"
bitCell
bitSortable="name"
[fn]="sortByName"
[class]="showExtraColumn ? 'lg:tw-w-3/5' : 'tw-w-full'"
>
{{ "name" | i18n }}
</th>
<!-- Individual vault -->
<th
*ngIf="!showAdminActions"
bitCell
[class]="showExtraColumn ? 'lg:tw-w-3/5' : 'tw-w-full'"
>
{{ "name" | i18n }}
</th>
<th bitCell *ngIf="showOwner" class="tw-hidden tw-w-2/5 lg:tw-table-cell">
{{ "owner" | i18n }}
</th>
<th bitCell class="tw-w-2/5" *ngIf="showCollections">{{ "collections" | i18n }}</th>
<th bitCell class="tw-w-2/5" *ngIf="showGroups">{{ "groups" | i18n }}</th>
<th bitCell class="tw-w-2/5" *ngIf="showPermissionsColumn">
<th bitCell bitSortable="groups" [fn]="sortByGroups" class="tw-w-2/5" *ngIf="showGroups">
{{ "groups" | i18n }}
</th>
<th
bitCell
bitSortable="permissions"
default="desc"
[fn]="sortByPermissions"
class="tw-w-2/5"
*ngIf="showPermissionsColumn"
>
{{ "permission" | i18n }}
</th>
<th bitCell class="tw-w-12 tw-text-right">

View File

@ -1,13 +1,17 @@
import { SelectionModel } from "@angular/cdk/collections";
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { CollectionView, Unassigned } from "@bitwarden/admin-console/common";
import { CollectionView, Unassigned, CollectionAdminView } from "@bitwarden/admin-console/common";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { TableDataSource } from "@bitwarden/components";
import { SortDirection, TableDataSource } from "@bitwarden/components";
import { GroupView } from "../../../admin-console/organizations/core";
import {
CollectionPermission,
convertToPermission,
} from "./../../../admin-console/organizations/shared/components/access-selector/access-selector.models";
import { VaultItem } from "./vault-item";
import { VaultItemEvent } from "./vault-item-event";
@ -17,6 +21,8 @@ export const RowHeightClass = `tw-h-[65px]`;
const MaxSelectionCount = 500;
type ItemPermission = CollectionPermission | "NoAccess";
@Component({
selector: "app-vault-items",
templateUrl: "vault-items.component.html",
@ -333,6 +339,119 @@ export class VaultItemsComponent {
return (canEditOrManageAllCiphers || this.allCiphersHaveEditAccess()) && collectionNotSelected;
}
/**
* Sorts VaultItems, grouping collections before ciphers, and sorting each group alphabetically by name.
*/
protected sortByName = (a: VaultItem, b: VaultItem, direction: SortDirection) => {
// Collections before ciphers
const collectionCompare = this.prioritizeCollections(a, b, direction);
if (collectionCompare !== 0) {
return collectionCompare;
}
return this.compareNames(a, b);
};
/**
* Sorts VaultItems based on group names
*/
protected sortByGroups = (a: VaultItem, b: VaultItem, direction: SortDirection) => {
if (
!(a.collection instanceof CollectionAdminView) &&
!(b.collection instanceof CollectionAdminView)
) {
return 0;
}
const getFirstGroupName = (collection: CollectionAdminView): string => {
if (collection.groups.length > 0) {
return collection.groups.map((group) => this.getGroupName(group.id) || "").sort()[0];
}
return null;
};
// Collections before ciphers
const collectionCompare = this.prioritizeCollections(a, b, direction);
if (collectionCompare !== 0) {
return collectionCompare;
}
const aGroupName = getFirstGroupName(a.collection as CollectionAdminView);
const bGroupName = getFirstGroupName(b.collection as CollectionAdminView);
// Collections with groups come before collections without groups.
// If a collection has no groups, getFirstGroupName returns null.
if (aGroupName === null) {
return 1;
}
if (bGroupName === null) {
return -1;
}
return aGroupName.localeCompare(bGroupName);
};
/**
* Sorts VaultItems based on their permissions, with higher permissions taking precedence.
* If permissions are equal, it falls back to sorting by name.
*/
protected sortByPermissions = (a: VaultItem, b: VaultItem, direction: SortDirection) => {
const getPermissionPriority = (item: VaultItem): number => {
const permission = item.collection
? this.getCollectionPermission(item.collection)
: this.getCipherPermission(item.cipher);
const priorityMap = {
[CollectionPermission.Manage]: 5,
[CollectionPermission.Edit]: 4,
[CollectionPermission.EditExceptPass]: 3,
[CollectionPermission.View]: 2,
[CollectionPermission.ViewExceptPass]: 1,
NoAccess: 0,
};
return priorityMap[permission] ?? -1;
};
// Collections before ciphers
const collectionCompare = this.prioritizeCollections(a, b, direction);
if (collectionCompare !== 0) {
return collectionCompare;
}
const priorityA = getPermissionPriority(a);
const priorityB = getPermissionPriority(b);
// Higher priority first
if (priorityA !== priorityB) {
return priorityA - priorityB;
}
return this.compareNames(a, b);
};
private compareNames(a: VaultItem, b: VaultItem): number {
const getName = (item: VaultItem) => item.collection?.name || item.cipher?.name;
return getName(a).localeCompare(getName(b));
}
/**
* Sorts VaultItems by prioritizing collections over ciphers.
* Collections are always placed before ciphers, regardless of the sorting direction.
*/
private prioritizeCollections(a: VaultItem, b: VaultItem, direction: SortDirection): number {
if (a.collection && !b.collection) {
return direction === "asc" ? -1 : 1;
}
if (!a.collection && b.collection) {
return direction === "asc" ? 1 : -1;
}
return 0;
}
private hasPersonalItems(): boolean {
return this.selection.selected.some(({ cipher }) => cipher?.organizationId === null);
}
@ -346,4 +465,58 @@ export class VaultItemsComponent {
private getUniqueOrganizationIds(): Set<string> {
return new Set(this.selection.selected.flatMap((i) => i.cipher?.organizationId ?? []));
}
private getGroupName(groupId: string): string | undefined {
return this.allGroups.find((g) => g.id === groupId)?.name;
}
private getCollectionPermission(collection: CollectionView): ItemPermission {
const organization = this.allOrganizations.find((o) => o.id === collection.organizationId);
if (collection.id == Unassigned && organization?.canEditUnassignedCiphers) {
return CollectionPermission.Edit;
}
if (collection.assigned) {
return convertToPermission(collection);
}
return "NoAccess";
}
private getCipherPermission(cipher: CipherView): ItemPermission {
if (!cipher.organizationId || cipher.collectionIds.length === 0) {
return CollectionPermission.Manage;
}
const filteredCollections = this.allCollections?.filter((collection) => {
if (collection.assigned) {
return cipher.collectionIds.find((id) => {
if (collection.id === id) {
return collection;
}
});
}
});
if (filteredCollections?.length === 1) {
return convertToPermission(filteredCollections[0]);
}
if (filteredCollections?.length > 0) {
const permissions = filteredCollections.map((collection) => convertToPermission(collection));
const orderedPermissions = [
CollectionPermission.Manage,
CollectionPermission.Edit,
CollectionPermission.EditExceptPass,
CollectionPermission.View,
CollectionPermission.ViewExceptPass,
];
return orderedPermissions.find((perm) => permissions.includes(perm));
}
return "NoAccess";
}
}

View File

@ -1,5 +1,5 @@
import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
import { combineLatest, map, Observable, Subject, takeUntil } from "rxjs";
import { combineLatest, map, Observable, of, Subject, switchMap, takeUntil } from "rxjs";
import {
OrganizationUserApiService,
@ -8,11 +8,14 @@ import {
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@ -53,6 +56,8 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy {
private resetPasswordService: OrganizationUserResetPasswordService,
private userVerificationService: UserVerificationService,
private toastService: ToastService,
private configService: ConfigService,
private organizationService: OrganizationService,
) {}
async ngOnInit() {
@ -60,23 +65,39 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy {
map((policies) => policies.filter((policy) => policy.type === PolicyType.ResetPassword)),
);
const managingOrg$ = this.configService
.getFeatureFlag$(FeatureFlag.AccountDeprovisioning)
.pipe(
switchMap((isAccountDeprovisioningEnabled) =>
isAccountDeprovisioningEnabled
? this.organizationService.organizations$.pipe(
map((organizations) =>
organizations.find((o) => o.userIsManagedByOrganization === true),
),
)
: of(null),
),
);
combineLatest([
this.organization$,
resetPasswordPolicies$,
this.userDecryptionOptionsService.userDecryptionOptions$,
managingOrg$,
])
.pipe(takeUntil(this.destroy$))
.subscribe(([organization, resetPasswordPolicies, decryptionOptions]) => {
.subscribe(([organization, resetPasswordPolicies, decryptionOptions, managingOrg]) => {
this.organization = organization;
this.resetPasswordPolicy = resetPasswordPolicies.find(
(p) => p.organizationId === organization.id,
);
// A user can leave an organization if they are NOT using TDE and Key Connector, or they have a master password.
// A user can leave an organization if they are NOT a managed user and they are NOT using TDE and Key Connector, or they have a master password.
this.showLeaveOrgOption =
(decryptionOptions.trustedDeviceOption == undefined &&
managingOrg?.id !== organization.id &&
((decryptionOptions.trustedDeviceOption == undefined &&
decryptionOptions.keyConnectorOption == undefined) ||
decryptionOptions.hasMasterPassword;
decryptionOptions.hasMasterPassword);
// Hide the 3 dot menu if the user has no available actions
this.hideMenu =

View File

@ -1,14 +1,13 @@
import { TestBed } from "@angular/core/testing";
import { BehaviorSubject } from "rxjs";
import { CollectionAdminService } from "@bitwarden/admin-console/common";
import { CollectionAdminService, CollectionAdminView } from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { CipherId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service";
@ -35,27 +34,41 @@ describe("AdminConsoleCipherFormConfigService", () => {
status: OrganizationUserStatusType.Confirmed,
};
const policyAppliesToActiveUser$ = new BehaviorSubject<boolean>(true);
const collection = {
id: "12345-5555",
organizationId: "234534-34334",
name: "Test Collection 1",
assigned: false,
readOnly: true,
} as CollectionAdminView;
const collection2 = {
id: "12345-6666",
organizationId: "22222-2222",
name: "Test Collection 2",
assigned: true,
readOnly: false,
} as CollectionAdminView;
const organization$ = new BehaviorSubject<Organization>(testOrg as Organization);
const organizations$ = new BehaviorSubject<Organization[]>([testOrg, testOrg2] as Organization[]);
const getCipherAdmin = jest.fn().mockResolvedValue(null);
const getCipher = jest.fn().mockResolvedValue(null);
beforeEach(async () => {
getCipherAdmin.mockClear();
getCipher.mockClear();
getCipher.mockResolvedValue({ id: cipherId, name: "Test Cipher - (non-admin)" });
getCipherAdmin.mockResolvedValue({ id: cipherId, name: "Test Cipher - (admin)" });
await TestBed.configureTestingModule({
providers: [
AdminConsoleCipherFormConfigService,
{ provide: OrganizationService, useValue: { get$: () => organization$, organizations$ } },
{
provide: CollectionAdminService,
useValue: { getAll: () => Promise.resolve([collection, collection2]) },
},
{
provide: PolicyService,
useValue: { policyAppliesToActiveUser$: () => policyAppliesToActiveUser$ },
},
{ provide: OrganizationService, useValue: { get$: () => organization$, organizations$ } },
{ provide: CipherService, useValue: { get: getCipher } },
{ provide: CollectionAdminService, useValue: { getAll: () => Promise.resolve([]) } },
{
provide: RoutedVaultFilterService,
useValue: { filter$: new BehaviorSubject({ organizationId: testOrg.id }) },
@ -86,6 +99,12 @@ describe("AdminConsoleCipherFormConfigService", () => {
expect(mode).toBe("edit");
});
it("returns all collections", async () => {
const { collections } = await adminConsoleConfigService.buildConfig("edit", cipherId);
expect(collections).toEqual([collection, collection2]);
});
it("sets admin flag based on `canEditAllCiphers`", async () => {
// Disable edit all ciphers on org
testOrg.canEditAllCiphers = false;
@ -153,33 +172,14 @@ describe("AdminConsoleCipherFormConfigService", () => {
expect(result.organizations).toEqual([testOrg, testOrg2]);
});
describe("getCipher", () => {
it("retrieves the cipher from the cipher service", async () => {
testOrg.canEditAllCiphers = false;
it("retrieves the cipher from the admin service", async () => {
getCipherAdmin.mockResolvedValue({ id: cipherId, name: "Test Cipher - (admin)" });
adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService);
adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService);
const result = await adminConsoleConfigService.buildConfig("clone", cipherId);
await adminConsoleConfigService.buildConfig("add", cipherId);
expect(getCipher).toHaveBeenCalledWith(cipherId);
expect(result.originalCipher.name).toBe("Test Cipher - (non-admin)");
// Admin service not needed when cipher service can return the cipher
expect(getCipherAdmin).not.toHaveBeenCalled();
});
it("retrieves the cipher from the admin service", async () => {
getCipher.mockResolvedValueOnce(null);
getCipherAdmin.mockResolvedValue({ id: cipherId, name: "Test Cipher - (admin)" });
adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService);
await adminConsoleConfigService.buildConfig("add", cipherId);
expect(getCipherAdmin).toHaveBeenCalledWith(cipherId);
expect(getCipher).toHaveBeenCalledWith(cipherId);
});
expect(getCipherAdmin).toHaveBeenCalledWith(cipherId);
});
});
});

View File

@ -6,9 +6,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType, OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { CipherId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
@ -25,7 +23,6 @@ import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/se
export class AdminConsoleCipherFormConfigService implements CipherFormConfigService {
private policyService: PolicyService = inject(PolicyService);
private organizationService: OrganizationService = inject(OrganizationService);
private cipherService: CipherService = inject(CipherService);
private routedVaultFilterService: RoutedVaultFilterService = inject(RoutedVaultFilterService);
private collectionAdminService: CollectionAdminService = inject(CollectionAdminService);
private apiService: ApiService = inject(ApiService);
@ -51,20 +48,8 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ
map(([orgs, orgId]) => orgs.find((o) => o.id === orgId)),
);
private editableCollections$ = this.organization$.pipe(
switchMap(async (org) => {
if (!org) {
return [];
}
const collections = await this.collectionAdminService.getAll(org.id);
// Users that can edit all ciphers can implicitly add to / edit within any collection
if (org.canEditAllCiphers) {
return collections;
}
// The user is only allowed to add/edit items to assigned collections that are not readonly
return collections.filter((c) => c.assigned && !c.readOnly);
}),
private allCollections$ = this.organization$.pipe(
switchMap(async (org) => await this.collectionAdminService.getAll(org.id)),
);
async buildConfig(
@ -72,21 +57,17 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ
cipherId?: CipherId,
cipherType?: CipherType,
): Promise<CipherFormConfig> {
const cipher = await this.getCipher(cipherId);
const [organization, allowPersonalOwnership, allOrganizations, allCollections] =
await firstValueFrom(
combineLatest([
this.organization$,
this.allowPersonalOwnership$,
this.allOrganizations$,
this.editableCollections$,
this.allCollections$,
]),
);
const cipher = await this.getCipher(organization, cipherId);
const collections = allCollections.filter(
(c) => c.organizationId === organization.id && c.assigned && !c.readOnly,
);
// When cloning from within the Admin Console, all organizations should be available.
// Otherwise only the one in context should be
const organizations = mode === "clone" ? allOrganizations : [organization];
@ -100,7 +81,7 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ
admin: organization.canEditAllCiphers ?? false,
allowPersonalOwnership: allowPersonalOwnershipOnlyForClone,
originalCipher: cipher,
collections,
collections: allCollections,
organizations,
folders: [], // folders not applicable in the admin console
hideIndividualVaultFields: true,
@ -108,19 +89,11 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ
};
}
private async getCipher(organization: Organization, id?: CipherId): Promise<Cipher | null> {
private async getCipher(id?: CipherId): Promise<Cipher | null> {
if (id == null) {
return Promise.resolve(null);
}
// Check to see if the user has direct access to the cipher
const cipherFromCipherService = await this.cipherService.get(id);
// If the organization doesn't allow admin/owners to edit all ciphers return the cipher
if (!organization.canEditAllCiphers && cipherFromCipherService != null) {
return cipherFromCipherService;
}
// Retrieve the cipher through the means of an admin
const cipherResponse = await this.apiService.getCipherAdmin(id);
cipherResponse.edit = true;

View File

@ -5,8 +5,8 @@
"criticalApplications": {
"message": "Critical applications"
},
"accessIntelligence": {
"message": "Access Intelligence"
"riskInsights": {
"message": "Risk Insights"
},
"passwordRisk": {
"message": "Password Risk"

View File

@ -1,9 +1,9 @@
import { Inject, Injectable } from "@angular/core";
// eslint-disable-next-line no-restricted-imports
import { mockCiphers } from "@bitwarden/bit-common/tools/reports/access-intelligence/services/ciphers.mock";
import { mockCiphers } from "@bitwarden/bit-common/tools/reports/risk-insights/services/ciphers.mock";
// eslint-disable-next-line no-restricted-imports
import { mockMemberCipherDetailsResponse } from "@bitwarden/bit-common/tools/reports/access-intelligence/services/member-cipher-details-response.mock";
import { mockMemberCipherDetailsResponse } from "@bitwarden/bit-common/tools/reports/risk-insights/services/member-cipher-details-response.mock";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";

View File

@ -41,6 +41,7 @@ module.exports = {
"<rootDir>/libs/platform/jest.config.js",
"<rootDir>/libs/node/jest.config.js",
"<rootDir>/libs/vault/jest.config.js",
"<rootDir>/libs/key-management/jest.config.js",
],
// Workaround for a memory leak that crashes tests in CI:

View File

@ -38,18 +38,20 @@
<div class="box-content">
<div
class="environment-selector-dialog"
data-testid="environment-selector-dialog"
[@transformPanel]="'open'"
cdkTrapFocus
cdkTrapFocusAutoCapture
role="dialog"
aria-modal="true"
>
<ng-container *ngFor="let region of availableRegions">
<ng-container *ngFor="let region of availableRegions; let i = index">
<button
type="button"
class="environment-selector-dialog-item"
(click)="toggle(region.key)"
[attr.aria-pressed]="data.selectedRegion === region ? 'true' : 'false'"
[attr.data-testid]="'environment-selector-dialog-item-' + i"
>
<i
class="bwi bwi-fw bwi-sm bwi-check"
@ -66,6 +68,7 @@
class="environment-selector-dialog-item"
(click)="toggle(ServerEnvironmentType.SelfHosted)"
[attr.aria-pressed]="data.selectedRegion ? 'false' : 'true'"
data-testid="environment-selector-dialog-item-self-hosted"
>
<i
class="bwi bwi-fw bwi-sm bwi-check"

View File

@ -178,6 +178,7 @@ import { BulkEncryptServiceImplementation } from "@bitwarden/common/platform/ser
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation";
import { DefaultBroadcasterService } from "@bitwarden/common/platform/services/default-broadcaster.service";
import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service";
import { DefaultServerSettingsService } from "@bitwarden/common/platform/services/default-server-settings.service";
import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service";
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
@ -1322,6 +1323,11 @@ const safeProviders: SafeProvider[] = [
InternalUserDecryptionOptionsServiceAbstraction,
],
}),
safeProvider({
provide: DefaultServerSettingsService,
useClass: DefaultServerSettingsService,
deps: [ConfigService],
}),
safeProvider({
provide: RegisterRouteService,
useClass: RegisterRouteService,

View File

@ -699,7 +699,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
}
protected deleteCipher() {
const asAdmin = this.organization?.canEditAllCiphers;
const asAdmin = this.organization?.canEditAllCiphers || !this.cipher.collectionIds;
return this.cipher.isDeleted
? this.cipherService.deleteWithServer(this.cipher.id, asAdmin)
: this.cipherService.softDeleteWithServer(this.cipher.id, asAdmin);

View File

@ -4,13 +4,14 @@ import { RouterModule } from "@angular/router";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { RegisterRouteService } from "@bitwarden/auth/common";
import { DefaultServerSettingsService } from "@bitwarden/common/platform/services/default-server-settings.service";
import { LinkModule } from "@bitwarden/components";
@Component({
standalone: true,
imports: [CommonModule, JslibModule, LinkModule, RouterModule],
template: `
<div class="tw-text-center">
<div class="tw-text-center" *ngIf="!(isUserRegistrationDisabled$ | async)">
{{ "newToBitwarden" | i18n }}
<a bitLink [routerLink]="registerRoute$ | async">{{ "createAccount" | i18n }}</a>
</div>
@ -18,7 +19,10 @@ import { LinkModule } from "@bitwarden/components";
})
export class LoginSecondaryContentComponent {
registerRouteService = inject(RegisterRouteService);
serverSettingsService = inject(DefaultServerSettingsService);
// TODO: remove when email verification flag is removed
protected registerRoute$ = this.registerRouteService.registerRoute$();
protected isUserRegistrationDisabled$ = this.serverSettingsService.isUserRegistrationDisabled$;
}

View File

@ -11,7 +11,7 @@
-->
<form [bitSubmit]="submit" [formGroup]="formGroup">
<ng-container *ngIf="loginUiState === LoginUiState.EMAIL_ENTRY">
<div [ngClass]="{ 'tw-invisible tw-h-0': loginUiState !== LoginUiState.EMAIL_ENTRY }">
<!-- Email Address input -->
<bit-form-field>
<bit-label>{{ "emailAddress" | i18n }}</bit-label>
@ -82,9 +82,9 @@
</button>
</ng-container>
</div>
</ng-container>
</div>
<ng-container *ngIf="loginUiState === LoginUiState.MASTER_PASSWORD_ENTRY">
<div [ngClass]="{ 'tw-invisible tw-h-0': loginUiState !== LoginUiState.MASTER_PASSWORD_ENTRY }">
<!-- Master Password input -->
<bit-form-field class="!tw-mb-1">
<bit-label>{{ "masterPass" | i18n }}</bit-label>
@ -140,5 +140,5 @@
</button>
</ng-container>
</div>
</ng-container>
</div>
</form>

View File

@ -2,7 +2,7 @@ import { CommonModule } from "@angular/common";
import { Component, ElementRef, Input, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from "@angular/forms";
import { ActivatedRoute, Router, RouterModule } from "@angular/router";
import { firstValueFrom, Subject, take, takeUntil } from "rxjs";
import { firstValueFrom, Subject, take, takeUntil, tap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import {
@ -19,9 +19,11 @@ import { CaptchaIFrame } from "@bitwarden/common/auth/captcha-iframe";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { ClientType, HttpStatusCode } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
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 { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -139,12 +141,16 @@ export class LoginComponent implements OnInit, OnDestroy {
private toastService: ToastService,
private logService: LogService,
private validationService: ValidationService,
private configService: ConfigService,
) {
this.clientType = this.platformUtilsService.getClientType();
this.loginViaAuthRequestSupported = this.loginComponentService.isLoginViaAuthRequestSupported();
}
async ngOnInit(): Promise<void> {
// TODO: remove this when the UnauthenticatedExtensionUIRefresh feature flag is removed.
this.listenForUnauthUiRefreshFlagChanges();
await this.defaultOnInit();
if (this.clientType === ClientType.Desktop) {
@ -162,6 +168,29 @@ export class LoginComponent implements OnInit, OnDestroy {
this.destroy$.complete();
}
private listenForUnauthUiRefreshFlagChanges() {
this.configService
.getFeatureFlag$(FeatureFlag.UnauthenticatedExtensionUIRefresh)
.pipe(
tap(async (flag) => {
// If the flag is turned OFF, we must force a reload to ensure the correct UI is shown
if (!flag) {
const uniqueQueryParams = {
...this.activatedRoute.queryParams,
// adding a unique timestamp to the query params to force a reload
t: new Date().getTime().toString(), // Adding a unique timestamp as a query parameter
};
await this.router.navigate(["/"], {
queryParams: uniqueQueryParams,
});
}
}),
takeUntil(this.destroy$),
)
.subscribe();
}
submit = async (): Promise<void> => {
if (this.clientType === ClientType.Desktop) {
if (this.loginUiState !== LoginUiState.MASTER_PASSWORD_ENTRY) {

View File

@ -1,7 +1,7 @@
<form [formGroup]="formGroup" *ngIf="!hideEnvSelector">
<bit-form-field>
<bit-label>{{ "creatingAccountOn" | i18n }}</bit-label>
<bit-select formControlName="selectedRegion">
<bit-select formControlName="selectedRegion" (closed)="onSelectClosed()">
<bit-option
*ngFor="let regionConfig of availableRegionConfigs"
[value]="regionConfig"

View File

@ -109,6 +109,9 @@ export class RegistrationEnvSelectorComponent implements OnInit, OnDestroy {
.subscribe();
}
/**
* Listens for changes to the selected region and updates the form value and emits the selected region.
*/
private listenForSelectedRegionChanges() {
this.selectedRegion.valueChanges
.pipe(
@ -124,16 +127,12 @@ export class RegistrationEnvSelectorComponent implements OnInit, OnDestroy {
return of(null);
}
if (selectedRegion === Region.SelfHosted) {
return from(SelfHostedEnvConfigDialogComponent.open(this.dialogService)).pipe(
tap((result: boolean | undefined) =>
this.handleSelfHostedEnvConfigDialogResult(result, prevSelectedRegion),
),
);
if (selectedRegion !== Region.SelfHosted) {
this.selectedRegionChange.emit(selectedRegion);
return from(this.environmentService.setEnvironment(selectedRegion.key));
}
this.selectedRegionChange.emit(selectedRegion);
return from(this.environmentService.setEnvironment(selectedRegion.key));
return of(null);
},
),
takeUntil(this.destroy$),
@ -170,6 +169,17 @@ export class RegistrationEnvSelectorComponent implements OnInit, OnDestroy {
}
}
/**
* Handles the event when the select is closed.
* If the selected region is self-hosted, opens the self-hosted environment settings dialog.
*/
protected async onSelectClosed() {
if (this.selectedRegion.value === Region.SelfHosted) {
const result = await SelfHostedEnvConfigDialogComponent.open(this.dialogService);
return this.handleSelfHostedEnvConfigDialogResult(result, this.selectedRegion.value);
}
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();

View File

@ -17,7 +17,6 @@ export enum FeatureFlag {
InlineMenuFieldQualification = "inline-menu-field-qualification",
MemberAccessReport = "ac-2059-member-access-report",
TwoFactorComponentRefactor = "two-factor-component-refactor",
EnableTimeThreshold = "PM-5864-dollar-threshold",
InlineMenuPositioningImprovements = "inline-menu-positioning-improvements",
ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner",
VaultBulkManagementAction = "vault-bulk-management-action",
@ -36,6 +35,7 @@ export enum FeatureFlag {
AccessIntelligence = "pm-13227-access-intelligence",
Pm13322AddPolicyDefinitions = "pm-13322-add-policy-definitions",
LimitCollectionCreationDeletionSplit = "pm-10863-limit-collection-creation-deletion-split",
CriticalApps = "pm-14466-risk-insights-critical-application",
DisableFreeFamiliesSponsorship = "PM-12274-disable-free-families-sponsorship",
}
@ -64,7 +64,6 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.InlineMenuFieldQualification]: FALSE,
[FeatureFlag.MemberAccessReport]: FALSE,
[FeatureFlag.TwoFactorComponentRefactor]: FALSE,
[FeatureFlag.EnableTimeThreshold]: FALSE,
[FeatureFlag.InlineMenuPositioningImprovements]: FALSE,
[FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE,
[FeatureFlag.VaultBulkManagementAction]: FALSE,
@ -83,6 +82,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.AccessIntelligence]: FALSE,
[FeatureFlag.Pm13322AddPolicyDefinitions]: FALSE,
[FeatureFlag.LimitCollectionCreationDeletionSplit]: FALSE,
[FeatureFlag.CriticalApps]: FALSE,
[FeatureFlag.DisableFreeFamiliesSponsorship]: FALSE,
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;

View File

@ -3,6 +3,7 @@ import { SemVer } from "semver";
import { FeatureFlag, FeatureFlagValueType } from "../../../enums/feature-flag.enum";
import { UserId } from "../../../types/guid";
import { ServerSettings } from "../../models/domain/server-settings";
import { Region } from "../environment.service";
import { ServerConfig } from "./server-config";
@ -10,6 +11,8 @@ import { ServerConfig } from "./server-config";
export abstract class ConfigService {
/** The server config of the currently active user */
serverConfig$: Observable<ServerConfig | null>;
/** The server settings of the currently active user */
serverSettings$: Observable<ServerSettings | null>;
/** The cloud region of the currently active user */
cloudRegion$: Observable<Region>;
/**

View File

@ -6,6 +6,7 @@ import {
ThirdPartyServerConfigData,
EnvironmentServerConfigData,
} from "../../models/data/server-config.data";
import { ServerSettings } from "../../models/domain/server-settings";
const dayInMilliseconds = 24 * 3600 * 1000;
@ -16,6 +17,7 @@ export class ServerConfig {
environment?: EnvironmentServerConfigData;
utcDate: Date;
featureStates: { [key: string]: AllowedFeatureFlagTypes } = {};
settings: ServerSettings;
constructor(serverConfigData: ServerConfigData) {
this.version = serverConfigData.version;
@ -24,6 +26,7 @@ export class ServerConfig {
this.utcDate = new Date(serverConfigData.utcDate);
this.environment = serverConfigData.environment;
this.featureStates = serverConfigData.featureStates;
this.settings = serverConfigData.settings;
if (this.server?.name == null && this.server?.url == null) {
this.server = null;

View File

@ -3,6 +3,7 @@
export type SharedFlags = {
showPasswordless?: boolean;
sdk?: boolean;
prereleaseBuild?: boolean;
};
// required to avoid linting errors when there are no flags

View File

@ -16,6 +16,9 @@ describe("ServerConfigData", () => {
name: "test",
url: "https://test.com",
},
settings: {
disableUserRegistration: false,
},
environment: {
cloudRegion: Region.EU,
vault: "https://vault.com",

View File

@ -2,6 +2,7 @@ import { Jsonify } from "type-fest";
import { AllowedFeatureFlagTypes } from "../../../enums/feature-flag.enum";
import { Region } from "../../abstractions/environment.service";
import { ServerSettings } from "../domain/server-settings";
import {
ServerConfigResponse,
ThirdPartyServerConfigResponse,
@ -15,6 +16,7 @@ export class ServerConfigData {
environment?: EnvironmentServerConfigData;
utcDate: string;
featureStates: { [key: string]: AllowedFeatureFlagTypes } = {};
settings: ServerSettings;
constructor(serverConfigResponse: Partial<ServerConfigResponse>) {
this.version = serverConfigResponse?.version;
@ -27,6 +29,7 @@ export class ServerConfigData {
? new EnvironmentServerConfigData(serverConfigResponse.environment)
: null;
this.featureStates = serverConfigResponse?.featureStates;
this.settings = new ServerSettings(serverConfigResponse.settings);
}
static fromJSON(obj: Jsonify<ServerConfigData>): ServerConfigData {

View File

@ -0,0 +1,20 @@
import { ServerSettings } from "./server-settings";
describe("ServerSettings", () => {
describe("disableUserRegistration", () => {
it("defaults disableUserRegistration to false", () => {
const settings = new ServerSettings();
expect(settings.disableUserRegistration).toBe(false);
});
it("sets disableUserRegistration to true when provided", () => {
const settings = new ServerSettings({ disableUserRegistration: true });
expect(settings.disableUserRegistration).toBe(true);
});
it("sets disableUserRegistration to false when provided", () => {
const settings = new ServerSettings({ disableUserRegistration: false });
expect(settings.disableUserRegistration).toBe(false);
});
});
});

View File

@ -0,0 +1,7 @@
export class ServerSettings {
disableUserRegistration: boolean;
constructor(data?: ServerSettings) {
this.disableUserRegistration = data?.disableUserRegistration ?? false;
}
}

View File

@ -1,6 +1,7 @@
import { AllowedFeatureFlagTypes } from "../../../enums/feature-flag.enum";
import { BaseResponse } from "../../../models/response/base.response";
import { Region } from "../../abstractions/environment.service";
import { ServerSettings } from "../domain/server-settings";
export class ServerConfigResponse extends BaseResponse {
version: string;
@ -8,6 +9,7 @@ export class ServerConfigResponse extends BaseResponse {
server: ThirdPartyServerConfigResponse;
environment: EnvironmentServerConfigResponse;
featureStates: { [key: string]: AllowedFeatureFlagTypes } = {};
settings: ServerSettings;
constructor(response: any) {
super(response);
@ -21,6 +23,7 @@ export class ServerConfigResponse extends BaseResponse {
this.server = new ThirdPartyServerConfigResponse(this.getResponseProperty("Server"));
this.environment = new EnvironmentServerConfigResponse(this.getResponseProperty("Environment"));
this.featureStates = this.getResponseProperty("FeatureStates");
this.settings = new ServerSettings(this.getResponseProperty("Settings"));
}
}

View File

@ -28,6 +28,7 @@ import { Environment, EnvironmentService, Region } from "../../abstractions/envi
import { LogService } from "../../abstractions/log.service";
import { devFlagEnabled, devFlagValue } from "../../misc/flags";
import { ServerConfigData } from "../../models/data/server-config.data";
import { ServerSettings } from "../../models/domain/server-settings";
import { CONFIG_DISK, KeyDefinition, StateProvider, UserKeyDefinition } from "../../state";
export const RETRIEVAL_INTERVAL = devFlagEnabled("configRetrievalIntervalMs")
@ -57,6 +58,8 @@ export class DefaultConfigService implements ConfigService {
serverConfig$: Observable<ServerConfig>;
serverSettings$: Observable<ServerSettings>;
cloudRegion$: Observable<Region>;
constructor(
@ -111,6 +114,10 @@ export class DefaultConfigService implements ConfigService {
this.cloudRegion$ = this.serverConfig$.pipe(
map((config) => config?.environment?.cloudRegion ?? Region.US),
);
this.serverSettings$ = this.serverConfig$.pipe(
map((config) => config?.settings ?? new ServerSettings()),
);
}
getFeatureFlag$<Flag extends FeatureFlag>(key: Flag) {

View File

@ -0,0 +1,47 @@
import { of } from "rxjs";
import { ConfigService } from "../abstractions/config/config.service";
import { ServerSettings } from "../models/domain/server-settings";
import { DefaultServerSettingsService } from "./default-server-settings.service";
describe("DefaultServerSettingsService", () => {
let service: DefaultServerSettingsService;
let configServiceMock: { serverSettings$: any };
beforeEach(() => {
configServiceMock = { serverSettings$: of() };
service = new DefaultServerSettingsService(configServiceMock as ConfigService);
});
describe("getSettings$", () => {
it("returns server settings", () => {
const mockSettings = new ServerSettings({ disableUserRegistration: true });
configServiceMock.serverSettings$ = of(mockSettings);
service.getSettings$().subscribe((settings) => {
expect(settings).toEqual(mockSettings);
});
});
});
describe("isUserRegistrationDisabled$", () => {
it("returns true when user registration is disabled", () => {
const mockSettings = new ServerSettings({ disableUserRegistration: true });
configServiceMock.serverSettings$ = of(mockSettings);
service.isUserRegistrationDisabled$.subscribe((isDisabled: boolean) => {
expect(isDisabled).toBe(true);
});
});
it("returns false when user registration is enabled", () => {
const mockSettings = new ServerSettings({ disableUserRegistration: false });
configServiceMock.serverSettings$ = of(mockSettings);
service.isUserRegistrationDisabled$.subscribe((isDisabled: boolean) => {
expect(isDisabled).toBe(false);
});
});
});
});

View File

@ -0,0 +1,19 @@
import { Observable } from "rxjs";
import { map } from "rxjs/operators";
import { ConfigService } from "../abstractions/config/config.service";
import { ServerSettings } from "../models/domain/server-settings";
export class DefaultServerSettingsService {
constructor(private configService: ConfigService) {}
getSettings$(): Observable<ServerSettings> {
return this.configService.serverSettings$;
}
get isUserRegistrationDisabled$(): Observable<boolean> {
return this.getSettings$().pipe(
map((settings: ServerSettings) => settings.disableUserRegistration),
);
}
}

View File

@ -126,6 +126,7 @@ import { AppIdService } from "../platform/abstractions/app-id.service";
import { EnvironmentService } from "../platform/abstractions/environment.service";
import { LogService } from "../platform/abstractions/log.service";
import { PlatformUtilsService } from "../platform/abstractions/platform-utils.service";
import { flagEnabled } from "../platform/misc/flags";
import { Utils } from "../platform/misc/utils";
import { SyncResponse } from "../platform/sync";
import { UserId } from "../types/guid";
@ -583,7 +584,7 @@ export class ApiService implements ApiServiceAbstraction {
}
putCipherCollectionsAdmin(id: string, request: CipherCollectionsRequest): Promise<any> {
return this.send("PUT", "/ciphers/" + id + "/collections-admin", request, true, false);
return this.send("PUT", "/ciphers/" + id + "/collections-admin", request, true, true);
}
postPurgeCiphers(
@ -1843,44 +1844,20 @@ export class ApiService implements ApiServiceAbstraction {
const requestUrl =
apiUrl + Utils.normalizePath(pathParts[0]) + (pathParts.length > 1 ? `?${pathParts[1]}` : "");
const headers = new Headers({
"Device-Type": this.deviceType,
});
if (this.customUserAgent != null) {
headers.set("User-Agent", this.customUserAgent);
}
const [requestHeaders, requestBody] = await this.buildHeadersAndBody(
authed,
hasResponse,
body,
alterHeaders,
);
const requestInit: RequestInit = {
cache: "no-store",
credentials: await this.getCredentials(),
method: method,
};
if (authed) {
const authHeader = await this.getActiveBearerToken();
headers.set("Authorization", "Bearer " + authHeader);
}
if (body != null) {
if (typeof body === "string") {
requestInit.body = body;
headers.set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8");
} else if (typeof body === "object") {
if (body instanceof FormData) {
requestInit.body = body;
} else {
headers.set("Content-Type", "application/json; charset=utf-8");
requestInit.body = JSON.stringify(body);
}
}
}
if (hasResponse) {
headers.set("Accept", "application/json");
}
if (alterHeaders != null) {
alterHeaders(headers);
}
requestInit.headers = headers;
requestInit.headers = requestHeaders;
requestInit.body = requestBody;
const response = await this.fetch(new Request(requestUrl, requestInit));
const responseType = response.headers.get("content-type");
@ -1897,6 +1874,51 @@ export class ApiService implements ApiServiceAbstraction {
}
}
private async buildHeadersAndBody(
authed: boolean,
hasResponse: boolean,
body: any,
alterHeaders: (headers: Headers) => void,
): Promise<[Headers, any]> {
let requestBody: any = null;
const headers = new Headers({
"Device-Type": this.deviceType,
});
if (flagEnabled("prereleaseBuild")) {
headers.set("Is-Prerelease", "1");
}
if (this.customUserAgent != null) {
headers.set("User-Agent", this.customUserAgent);
}
if (hasResponse) {
headers.set("Accept", "application/json");
}
if (alterHeaders != null) {
alterHeaders(headers);
}
if (authed) {
const authHeader = await this.getActiveBearerToken();
headers.set("Authorization", "Bearer " + authHeader);
}
if (body != null) {
if (typeof body === "string") {
requestBody = body;
headers.set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8");
} else if (typeof body === "object") {
if (body instanceof FormData) {
requestBody = body;
} else {
headers.set("Content-Type", "application/json; charset=utf-8");
requestBody = JSON.stringify(body);
}
}
}
return [headers, requestBody];
}
private async handleError(
response: Response,
tokenError: boolean,

View File

@ -22,6 +22,7 @@ export type ObjectKey<State, Secret = State, Disclosed = Record<string, never>>
classifier: Classifier<State, Disclosed, Secret>;
format: "plain" | "classified";
options: UserKeyDefinitionOptions<State>;
initial?: State;
};
export function isObjectKey(key: any): key is ObjectKey<unknown> {

View File

@ -254,17 +254,18 @@ export class UserStateSubject<
withConstraints,
map(([loadedState, constraints]) => {
// bypass nulls
if (!loadedState) {
if (!loadedState && !this.objectKey?.initial) {
return {
constraints: {} as Constraints<State>,
state: null,
} satisfies Constrained<State>;
}
const unconstrained = loadedState ?? structuredClone(this.objectKey.initial);
const calibration = isDynamic(constraints)
? constraints.calibrate(loadedState)
? constraints.calibrate(unconstrained)
: constraints;
const adjusted = calibration.adjust(loadedState);
const adjusted = calibration.adjust(unconstrained);
return {
constraints: calibration.constraints,

View File

@ -119,7 +119,7 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
* Used for Unassigned ciphers or when the user only has admin access to the cipher (not assigned normally).
* @param cipher
*/
saveCollectionsWithServerAdmin: (cipher: Cipher) => Promise<void>;
saveCollectionsWithServerAdmin: (cipher: Cipher) => Promise<Cipher>;
/**
* Bulk update collections for many ciphers with the server
* @param orgId

View File

@ -880,9 +880,11 @@ export class CipherService implements CipherServiceAbstraction {
return new Cipher(updated[cipher.id as CipherId], cipher.localData);
}
async saveCollectionsWithServerAdmin(cipher: Cipher): Promise<void> {
async saveCollectionsWithServerAdmin(cipher: Cipher): Promise<Cipher> {
const request = new CipherCollectionsRequest(cipher.collectionIds);
await this.apiService.putCipherCollectionsAdmin(cipher.id, request);
const response = await this.apiService.putCipherCollectionsAdmin(cipher.id, request);
const data = new CipherData(response);
return new Cipher(data);
}
/**

View File

@ -0,0 +1,27 @@
import { Directive, HostBinding, HostListener, Input } from "@angular/core";
import { DisclosureComponent } from "./disclosure.component";
@Directive({
selector: "[bitDisclosureTriggerFor]",
exportAs: "disclosureTriggerFor",
standalone: true,
})
export class DisclosureTriggerForDirective {
/**
* Accepts template reference for a bit-disclosure component instance
*/
@Input("bitDisclosureTriggerFor") disclosure: DisclosureComponent;
@HostBinding("attr.aria-expanded") get ariaExpanded() {
return this.disclosure.open;
}
@HostBinding("attr.aria-controls") get ariaControls() {
return this.disclosure.id;
}
@HostListener("click") click() {
this.disclosure.open = !this.disclosure.open;
}
}

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