mirror of
https://github.com/bitwarden/browser
synced 2026-02-11 05:53:42 +00:00
Merge branch 'main' into fix-ci
This commit is contained in:
130
.github/workflows/deploy-web.yml
vendored
130
.github/workflows/deploy-web.yml
vendored
@@ -63,14 +63,14 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
environment: ${{ steps.config.outputs.environment }}
|
||||
environment-url: ${{ steps.config.outputs.environment-url }}
|
||||
environment-name: ${{ steps.config.outputs.environment-name }}
|
||||
environment-artifact: ${{ steps.config.outputs.environment-artifact }}
|
||||
azure-login-creds: ${{ steps.config.outputs.azure-login-creds }}
|
||||
retrieve-secrets-keyvault: ${{ steps.config.outputs.retrieve-secrets-keyvault }}
|
||||
sync-utility: ${{ steps.config.outputs.sync-utility }}
|
||||
sync-delete-destination-files: ${{ steps.config.outputs.sync-delete-destination-files }}
|
||||
slack-channel-name: ${{ steps.config.outputs.slack-channel-name }}
|
||||
environment_url: ${{ steps.config.outputs.environment_url }}
|
||||
environment_name: ${{ steps.config.outputs.environment_name }}
|
||||
environment_artifact: ${{ steps.config.outputs.environment_artifact }}
|
||||
azure_login_creds: ${{ steps.config.outputs.azure_login_creds }}
|
||||
retrive_secrets_keyvault: ${{ steps.config.outputs.retrive_secrets_keyvault }}
|
||||
sync_utility: ${{ steps.config.outputs.sync_utility }}
|
||||
sync_delete_destination_files: ${{ steps.config.outputs.sync_delete_destination_files }}
|
||||
slack_channel_name: ${{ steps.config.outputs.slack-channel-name }}
|
||||
steps:
|
||||
- name: Configure
|
||||
id: config
|
||||
@@ -81,48 +81,48 @@ jobs:
|
||||
|
||||
case ${{ inputs.environment }} in
|
||||
"USQA")
|
||||
echo "azure-login-creds=AZURE_KV_US_QA_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT
|
||||
echo "retrieve-secrets-keyvault=bw-webvault-rlktusqa-kv" >> $GITHUB_OUTPUT
|
||||
echo "environment-artifact=web-*-cloud-QA.zip" >> $GITHUB_OUTPUT
|
||||
echo "environment-name=Web Vault - US QA Cloud" >> $GITHUB_OUTPUT
|
||||
echo "environment-url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT
|
||||
echo "slack-channel-name=alerts-deploy-qa" >> $GITHUB_OUTPUT
|
||||
echo "azure_login_creds=AZURE_KV_US_QA_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT
|
||||
echo "retrive_secrets_keyvault=bw-webvault-rlktusqa-kv" >> $GITHUB_OUTPUT
|
||||
echo "environment_artifact=web-*-cloud-QA.zip" >> $GITHUB_OUTPUT
|
||||
echo "environment_name=Web Vault - US QA Cloud" >> $GITHUB_OUTPUT
|
||||
echo "environment_url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT
|
||||
echo "slack_channel_name=alerts-deploy-qa" >> $GITHUB_OUTPUT
|
||||
;;
|
||||
"EUQA")
|
||||
echo "azure-login-creds=AZURE_KV_EU_QA_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT
|
||||
echo "retrieve-secrets-keyvault=webvaulteu-westeurope-qa" >> $GITHUB_OUTPUT
|
||||
echo "environment-artifact=web-*-cloud-euqa.zip" >> $GITHUB_OUTPUT
|
||||
echo "environment-name=Web Vault - EU QA Cloud" >> $GITHUB_OUTPUT
|
||||
echo "environment-url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT
|
||||
echo "slack-channel-name=alerts-deploy-qa" >> $GITHUB_OUTPUT
|
||||
echo "azure_login_creds=AZURE_KV_EU_QA_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT
|
||||
echo "retrive_secrets_keyvault=webvaulteu-westeurope-qa" >> $GITHUB_OUTPUT
|
||||
echo "environment_artifact=web-*-cloud-euqa.zip" >> $GITHUB_OUTPUT
|
||||
echo "environment_name=Web Vault - EU QA Cloud" >> $GITHUB_OUTPUT
|
||||
echo "environment_url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT
|
||||
echo "slack_channel_name=alerts-deploy-qa" >> $GITHUB_OUTPUT
|
||||
;;
|
||||
"USPROD")
|
||||
echo "azure-login-creds=AZURE_KV_US_PROD_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT
|
||||
echo "retrieve-secrets-keyvault=bw-webvault-klrt-kv" >> $GITHUB_OUTPUT
|
||||
echo "environment-artifact=web-*-cloud-COMMERCIAL.zip" >> $GITHUB_OUTPUT
|
||||
echo "environment-name=Web Vault - US Production Cloud" >> $GITHUB_OUTPUT
|
||||
echo "environment-url=http://vault.bitwarden.com" >> $GITHUB_OUTPUT
|
||||
echo "slack-channel-name=alerts-deploy-prd" >> $GITHUB_OUTPUT
|
||||
echo "azure_login_creds=AZURE_KV_US_PROD_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT
|
||||
echo "retrive_secrets_keyvault=bw-webvault-klrt-kv" >> $GITHUB_OUTPUT
|
||||
echo "environment_artifact=web-*-cloud-COMMERCIAL.zip" >> $GITHUB_OUTPUT
|
||||
echo "environment_name=Web Vault - US Production Cloud" >> $GITHUB_OUTPUT
|
||||
echo "environment_url=http://vault.bitwarden.com" >> $GITHUB_OUTPUT
|
||||
echo "slack_channel_name=alerts-deploy-prd" >> $GITHUB_OUTPUT
|
||||
;;
|
||||
"EUPROD")
|
||||
echo "azure-login-creds=AZURE_KV_EU_PRD_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT
|
||||
echo "retrieve-secrets-keyvault=webvault-westeurope-prod" >> $GITHUB_OUTPUT
|
||||
echo "environment-artifact=web-*-cloud-euprd.zip" >> $GITHUB_OUTPUT
|
||||
echo "environment-name=Web Vault - EU Production Cloud" >> $GITHUB_OUTPUT
|
||||
echo "environment-url=http://vault.bitwarden.eu" >> $GITHUB_OUTPUT
|
||||
echo "slack-channel-name=alerts-deploy-prd" >> $GITHUB_OUTPUT
|
||||
echo "azure_login_creds=AZURE_KV_EU_PRD_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT
|
||||
echo "retrive_secrets_keyvault=webvault-westeurope-prod" >> $GITHUB_OUTPUT
|
||||
echo "environment_artifact=web-*-cloud-euprd.zip" >> $GITHUB_OUTPUT
|
||||
echo "environment_name=Web Vault - EU Production Cloud" >> $GITHUB_OUTPUT
|
||||
echo "environment_url=http://vault.bitwarden.eu" >> $GITHUB_OUTPUT
|
||||
echo "slack_channel_name=alerts-deploy-prd" >> $GITHUB_OUTPUT
|
||||
;;
|
||||
"USDEV")
|
||||
echo "azure-login-creds=AZURE_KV_US_DEV_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT
|
||||
echo "retrieve-secrets-keyvault=webvault-eastus-dev" >> $GITHUB_OUTPUT
|
||||
echo "environment-artifact=web-*-cloud-usdev.zip" >> $GITHUB_OUTPUT
|
||||
echo "environment-name=Web Vault - US Development Cloud" >> $GITHUB_OUTPUT
|
||||
echo "environment-url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT
|
||||
echo "slack-channel-name=alerts-deploy-dev" >> $GITHUB_OUTPUT
|
||||
echo "azure_login_creds=AZURE_KV_US_DEV_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT
|
||||
echo "retrive_secrets_keyvault=webvault-eastus-dev" >> $GITHUB_OUTPUT
|
||||
echo "environment_artifact=web-*-cloud-usdev.zip" >> $GITHUB_OUTPUT
|
||||
echo "environment_name=Web Vault - US Development Cloud" >> $GITHUB_OUTPUT
|
||||
echo "environment_url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT
|
||||
echo "slack_channel_name=alerts-deploy-dev" >> $GITHUB_OUTPUT
|
||||
;;
|
||||
esac
|
||||
# Set the sync utility to use for deployment to the environment (az-sync or azcopy)
|
||||
echo "sync-utility=azcopy" >> $GITHUB_OUTPUT
|
||||
echo "sync_utility=azcopy" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Environment Protection
|
||||
env:
|
||||
@@ -168,10 +168,10 @@ jobs:
|
||||
fi
|
||||
|
||||
approval:
|
||||
name: Approval for Deployment to ${{ needs.setup.outputs.environment-name }}
|
||||
name: Approval for Deployment to ${{ needs.setup.outputs.environment_name }}
|
||||
needs: setup
|
||||
runs-on: ubuntu-22.04
|
||||
environment: ${{ needs.setup.outputs.environment-name }}
|
||||
environment: ${{ needs.setup.outputs.environment_name }}
|
||||
steps:
|
||||
- name: Success Code
|
||||
run: exit 0
|
||||
@@ -181,9 +181,9 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
needs: setup
|
||||
env:
|
||||
_ENVIRONMENT_ARTIFACT: ${{ needs.setup.outputs.environment-artifact }}
|
||||
_ENVIRONMENT_ARTIFACT: ${{ needs.setup.outputs.environment_artifact }}
|
||||
outputs:
|
||||
artifact-build-commit: ${{ steps.set-artifact-commit.outputs.commit }}
|
||||
artifact_build_commit: ${{ steps.set-artifact-commit.outputs.commit }}
|
||||
steps:
|
||||
- name: 'Download latest cloud asset using GitHub Run ID: ${{ inputs.build-web-run-id }}'
|
||||
if: ${{ inputs.build-web-run-id }}
|
||||
@@ -242,7 +242,7 @@ jobs:
|
||||
run: |
|
||||
# If run-id was used, get the commit from the download-latest-artifacts-run-id step
|
||||
if [ "${{ inputs.build-web-run-id }}" ]; then
|
||||
echo "commit=${{ steps.download-latest-artifacts-run-id.outputs.artifact-build-commit }}" >> $GITHUB_OUTPUT
|
||||
echo "commit=${{ steps.download-latest-artifacts-run-id.outputs.artifact_build_commit }}" >> $GITHUB_OUTPUT
|
||||
|
||||
elif [ "${{ steps.download-latest-artifacts.outcome }}" == "failure" ]; then
|
||||
# If the download-latest-artifacts step failed, query the GH API to get the commit SHA of the artifact that was just built with trigger-build-web.
|
||||
@@ -251,7 +251,7 @@ jobs:
|
||||
|
||||
else
|
||||
# Set the commit to the output of step download-latest-artifacts.
|
||||
echo "commit=${{ steps.download-latest-artifacts.outputs.artifact-build-commit }}" >> $GITHUB_OUTPUT
|
||||
echo "commit=${{ steps.download-latest-artifacts.outputs.artifact_build_commit }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
notify-start:
|
||||
@@ -271,11 +271,11 @@ jobs:
|
||||
id: slack-message
|
||||
with:
|
||||
project: Clients
|
||||
environment: ${{ needs.setup.outputs.environment-name }}
|
||||
environment: ${{ needs.setup.outputs.environment_name }}
|
||||
tag: ${{ inputs.branch-or-tag }}
|
||||
slack-channel: ${{ needs.setup.outputs.slack-channel-name }}
|
||||
slack-channel: ${{ needs.setup.outputs.slack_channel_name }}
|
||||
event: 'start'
|
||||
commit-sha: ${{ needs.artifact-check.outputs.artifact-build-commit }}
|
||||
commit-sha: ${{ needs.artifact-check.outputs.artifact_build_commit }}
|
||||
url: https://github.com/bitwarden/clients/actions/runs/${{ github.run_id }}
|
||||
AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
@@ -287,7 +287,7 @@ jobs:
|
||||
- name: Display commit SHA
|
||||
run: |
|
||||
REPO_URL="https://github.com/bitwarden/clients/commit"
|
||||
COMMIT_SHA="${{ needs.artifact-check.outputs.artifact-build-commit }}"
|
||||
COMMIT_SHA="${{ needs.artifact-check.outputs.artifact_build_commit }}"
|
||||
echo ":steam_locomotive: View [commit]($REPO_URL/$COMMIT_SHA)" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
azure-deploy:
|
||||
@@ -299,9 +299,9 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
env:
|
||||
_ENVIRONMENT: ${{ needs.setup.outputs.environment }}
|
||||
_ENVIRONMENT_URL: ${{ needs.setup.outputs.environment-url }}
|
||||
_ENVIRONMENT_NAME: ${{ needs.setup.outputs.environment-name }}
|
||||
_ENVIRONMENT_ARTIFACT: ${{ needs.setup.outputs.environment-artifact }}
|
||||
_ENVIRONMENT_URL: ${{ needs.setup.outputs.environment_url }}
|
||||
_ENVIRONMENT_NAME: ${{ needs.setup.outputs.environment_name }}
|
||||
_ENVIRONMENT_ARTIFACT: ${{ needs.setup.outputs.environment_artifact }}
|
||||
steps:
|
||||
- name: Create GitHub deployment
|
||||
uses: chrnorm/deployment-action@55729fcebec3d284f60f5bcabbd8376437d696b1 # v2.0.7
|
||||
@@ -309,31 +309,31 @@ jobs:
|
||||
with:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
initial-status: 'in_progress'
|
||||
environment-url: ${{ env._ENVIRONMENT_URL }}
|
||||
environment_url: ${{ env._ENVIRONMENT_URL }}
|
||||
environment: ${{ env._ENVIRONMENT_NAME }}
|
||||
task: 'deploy'
|
||||
description: 'Deployment from branch/tag: ${{ inputs.branch-or-tag }}'
|
||||
ref: ${{ needs.artifact-check.outputs.artifact-build-commit }}
|
||||
ref: ${{ needs.artifact-check.outputs.artifact_build_commit }}
|
||||
|
||||
- name: Login to Azure
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
with:
|
||||
creds: ${{ secrets[needs.setup.outputs.azure-login-creds] }}
|
||||
creds: ${{ secrets[needs.setup.outputs.azure_login_creds] }}
|
||||
|
||||
- name: Retrieve Storage Account connection string for az sync
|
||||
if: ${{ needs.setup.outputs.sync-utility == 'az-sync' }}
|
||||
if: ${{ needs.setup.outputs.sync_utility == 'az-sync' }}
|
||||
id: retrieve-secrets-az-sync
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: ${{ needs.setup.outputs.retrieve-secrets-keyvault }}
|
||||
keyvault: ${{ needs.setup.outputs.retrive_secrets_keyvault }}
|
||||
secrets: "sa-bitwarden-web-vault-dev-key-temp"
|
||||
|
||||
- name: Retrieve Storage Account name and SPN credentials for azcopy
|
||||
if: ${{ needs.setup.outputs.sync-utility == 'azcopy' }}
|
||||
if: ${{ needs.setup.outputs.sync_utility == 'azcopy' }}
|
||||
id: retrieve-secrets-azcopy
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: ${{ needs.setup.outputs.retrieve-secrets-keyvault }}
|
||||
keyvault: ${{ needs.setup.outputs.retrive_secrets_keyvault }}
|
||||
secrets: "sa-bitwarden-web-vault-name,sp-bitwarden-web-vault-password,sp-bitwarden-web-vault-appid,sp-bitwarden-web-vault-tenant"
|
||||
|
||||
- name: 'Download latest cloud asset using GitHub Run ID: ${{ inputs.build-web-run-id }}'
|
||||
@@ -363,7 +363,7 @@ jobs:
|
||||
run: unzip ${{ env._ENVIRONMENT_ARTIFACT }}
|
||||
|
||||
- name: Sync to Azure Storage Account using az storage blob sync
|
||||
if: ${{ needs.setup.outputs.sync-utility == 'az-sync' }}
|
||||
if: ${{ needs.setup.outputs.sync_utility == 'az-sync' }}
|
||||
working-directory: apps/web
|
||||
run: |
|
||||
az storage blob sync \
|
||||
@@ -373,7 +373,7 @@ jobs:
|
||||
--delete-destination=${{ inputs.force-delete-destination }}
|
||||
|
||||
- name: Sync to Azure Storage Account using azcopy
|
||||
if: ${{ needs.setup.outputs.sync-utility == 'azcopy' }}
|
||||
if: ${{ needs.setup.outputs.sync_utility == 'azcopy' }}
|
||||
working-directory: apps/web
|
||||
env:
|
||||
AZCOPY_AUTO_LOGIN_TYPE: SPN
|
||||
@@ -397,7 +397,7 @@ jobs:
|
||||
uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3
|
||||
with:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
environment-url: ${{ env._ENVIRONMENT_URL }}
|
||||
environment_url: ${{ env._ENVIRONMENT_URL }}
|
||||
state: 'success'
|
||||
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
|
||||
|
||||
@@ -406,7 +406,7 @@ jobs:
|
||||
uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3
|
||||
with:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
environment-url: ${{ env._ENVIRONMENT_URL }}
|
||||
environment_url: ${{ env._ENVIRONMENT_URL }}
|
||||
state: 'failure'
|
||||
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
|
||||
|
||||
@@ -424,11 +424,11 @@ jobs:
|
||||
uses: bitwarden/gh-actions/report-deployment-status-to-slack@main
|
||||
with:
|
||||
project: Clients
|
||||
environment: ${{ needs.setup.outputs.environment-name }}
|
||||
environment: ${{ needs.setup.outputs.environment_name }}
|
||||
tag: ${{ inputs.branch-or-tag }}
|
||||
slack-channel: ${{ needs.notify-start.outputs.channel_id }}
|
||||
event: ${{ needs.azure-deploy.result }}
|
||||
url: https://github.com/bitwarden/clients/actions/runs/${{ github.run_id }}
|
||||
commit-sha: ${{ needs.artifact-check.outputs.artifact-build-commit }}
|
||||
commit-sha: ${{ needs.artifact-check.outputs.artifact_build_commit }}
|
||||
update-ts: ${{ needs.notify-start.outputs.ts }}
|
||||
AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
18
.github/workflows/lint.yml
vendored
18
.github/workflows/lint.yml
vendored
@@ -54,21 +54,25 @@ jobs:
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
node-version: ${{ steps.retrieve-node-version.outputs.node_version }}
|
||||
|
||||
- name: Install Node dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Lint unowned dependencies
|
||||
run: npm run lint:dep-ownership
|
||||
|
||||
- name: Run linter
|
||||
run: |
|
||||
npm ci
|
||||
npm run lint
|
||||
run: npm run lint
|
||||
|
||||
rust:
|
||||
name: Run Rust lint on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os || 'ubuntu-latest' }}
|
||||
runs-on: ${{ matrix.os || 'ubuntu-24.04' }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- macos-latest
|
||||
- windows-latest
|
||||
- ubuntu-24.04
|
||||
- macos-14
|
||||
- windows-2022
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
|
||||
14
.github/workflows/publish-cli.yml
vendored
14
.github/workflows/publish-cli.yml
vendored
@@ -43,8 +43,8 @@ jobs:
|
||||
name: Setup
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
release-version: ${{ steps.version-output.outputs.version }}
|
||||
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
|
||||
release_version: ${{ steps.version-output.outputs.version }}
|
||||
deployment_id: ${{ steps.deployment.outputs.deployment_id }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: .
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
needs: setup
|
||||
if: inputs.snap_publish
|
||||
env:
|
||||
_PKG_VERSION: ${{ needs.setup.outputs.release-version }}
|
||||
_PKG_VERSION: ${{ needs.setup.outputs.release_version }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
@@ -125,7 +125,7 @@ jobs:
|
||||
needs: setup
|
||||
if: inputs.choco_publish
|
||||
env:
|
||||
_PKG_VERSION: ${{ needs.setup.outputs.release-version }}
|
||||
_PKG_VERSION: ${{ needs.setup.outputs.release_version }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
@@ -165,7 +165,7 @@ jobs:
|
||||
needs: setup
|
||||
if: inputs.npm_publish
|
||||
env:
|
||||
_PKG_VERSION: ${{ needs.setup.outputs.release-version }}
|
||||
_PKG_VERSION: ${{ needs.setup.outputs.release_version }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
@@ -222,7 +222,7 @@ jobs:
|
||||
with:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
state: 'success'
|
||||
deployment-id: ${{ needs.setup.outputs.deployment-id }}
|
||||
deployment_id: ${{ needs.setup.outputs.deployment_id }}
|
||||
|
||||
- name: Update deployment status to Failure
|
||||
if: ${{ inputs.publish_type != 'Dry Run' && failure() }}
|
||||
@@ -230,4 +230,4 @@ jobs:
|
||||
with:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
state: 'failure'
|
||||
deployment-id: ${{ needs.setup.outputs.deployment-id }}
|
||||
deployment_id: ${{ needs.setup.outputs.deployment_id }}
|
||||
|
||||
34
.github/workflows/publish-desktop.yml
vendored
34
.github/workflows/publish-desktop.yml
vendored
@@ -39,10 +39,10 @@ jobs:
|
||||
name: Setup
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
release-version: ${{ steps.version.outputs.version }}
|
||||
release-channel: ${{ steps.release-channel.outputs.channel }}
|
||||
tag-name: ${{ steps.version.outputs.tag_name }}
|
||||
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
|
||||
release_version: ${{ steps.version.outputs.version }}
|
||||
release_channel: ${{ steps.release_channel.outputs.channel }}
|
||||
tag_name: ${{ steps.version.outputs.tag_name }}
|
||||
deployment_id: ${{ steps.deployment.outputs.deployment_id }}
|
||||
steps:
|
||||
- name: Branch check
|
||||
if: ${{ inputs.publish_type != 'Dry Run' }}
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Get Version Channel
|
||||
id: release-channel
|
||||
id: release_channel
|
||||
run: |
|
||||
case "${{ steps.version.outputs.version }}" in
|
||||
*"alpha"*)
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
initial-status: 'in_progress'
|
||||
environment: 'Desktop - Production'
|
||||
description: 'Deployment ${{ steps.version.outputs.version }} to channel ${{ steps.release-channel.outputs.channel }} from branch ${{ github.ref_name }}'
|
||||
description: 'Deployment ${{ steps.version.outputs.version }} to channel ${{ steps.release_channel.outputs.channel }} from branch ${{ github.ref_name }}'
|
||||
task: release
|
||||
|
||||
electron-blob:
|
||||
@@ -108,8 +108,8 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
needs: setup
|
||||
env:
|
||||
_PKG_VERSION: ${{ needs.setup.outputs.release-version }}
|
||||
_RELEASE_TAG: ${{ needs.setup.outputs.tag-name }}
|
||||
_PKG_VERSION: ${{ needs.setup.outputs.release_version }}
|
||||
_RELEASE_TAG: ${{ needs.setup.outputs.tag_name }}
|
||||
steps:
|
||||
- name: Login to Azure
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
@@ -136,7 +136,7 @@ jobs:
|
||||
|
||||
- name: Set staged rollout percentage
|
||||
env:
|
||||
RELEASE_CHANNEL: ${{ needs.setup.outputs.release-channel }}
|
||||
RELEASE_CHANNEL: ${{ needs.setup.outputs.release_channel }}
|
||||
ROLLOUT_PCT: ${{ inputs.rollout_percentage }}
|
||||
run: |
|
||||
echo "stagingPercentage: ${ROLLOUT_PCT}" >> apps/desktop/artifacts/${RELEASE_CHANNEL}.yml
|
||||
@@ -163,7 +163,7 @@ jobs:
|
||||
with:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
state: 'success'
|
||||
deployment-id: ${{ needs.setup.outputs.deployment-id }}
|
||||
deployment_id: ${{ needs.setup.outputs.deployment_id }}
|
||||
|
||||
- name: Update deployment status to Failure
|
||||
if: ${{ inputs.publish_type != 'Dry Run' && failure() }}
|
||||
@@ -171,7 +171,7 @@ jobs:
|
||||
with:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
state: 'failure'
|
||||
deployment-id: ${{ needs.setup.outputs.deployment-id }}
|
||||
deployment_id: ${{ needs.setup.outputs.deployment_id }}
|
||||
|
||||
snap:
|
||||
name: Deploy Snap
|
||||
@@ -179,8 +179,8 @@ jobs:
|
||||
needs: setup
|
||||
if: inputs.snap_publish
|
||||
env:
|
||||
_PKG_VERSION: ${{ needs.setup.outputs.release-version }}
|
||||
_RELEASE_TAG: ${{ needs.setup.outputs.tag-name }}
|
||||
_PKG_VERSION: ${{ needs.setup.outputs.release_version }}
|
||||
_RELEASE_TAG: ${{ needs.setup.outputs.tag_name }}
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
@@ -223,8 +223,8 @@ jobs:
|
||||
needs: setup
|
||||
if: inputs.choco_publish
|
||||
env:
|
||||
_PKG_VERSION: ${{ needs.setup.outputs.release-version }}
|
||||
_RELEASE_TAG: ${{ needs.setup.outputs.tag-name }}
|
||||
_PKG_VERSION: ${{ needs.setup.outputs.release_version }}
|
||||
_RELEASE_TAG: ${{ needs.setup.outputs.tag_name }}
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
@@ -284,7 +284,7 @@ jobs:
|
||||
with:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
state: 'success'
|
||||
deployment-id: ${{ needs.setup.outputs.deployment-id }}
|
||||
deployment_id: ${{ needs.setup.outputs.deployment_id }}
|
||||
|
||||
- name: Update deployment status to Failure
|
||||
if: ${{ inputs.publish_type != 'Dry Run' && failure() }}
|
||||
@@ -292,4 +292,4 @@ jobs:
|
||||
with:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
state: 'failure'
|
||||
deployment-id: ${{ needs.setup.outputs.deployment-id }}
|
||||
deployment_id: ${{ needs.setup.outputs.deployment_id }}
|
||||
|
||||
20
.github/workflows/release-browser.yml
vendored
20
.github/workflows/release-browser.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
name: Setup
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
release-version: ${{ steps.version.outputs.version }}
|
||||
release_version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
|
||||
- name: Check Release Version
|
||||
id: version
|
||||
uses: bitwarden/gh-actions/release-version-check@main
|
||||
uses: bitwarden/gh-actions/release_version-check@main
|
||||
with:
|
||||
release-type: ${{ github.event.inputs.release_type }}
|
||||
project-type: ts
|
||||
@@ -118,7 +118,7 @@ jobs:
|
||||
|
||||
- name: Rename build artifacts
|
||||
env:
|
||||
PACKAGE_VERSION: ${{ needs.setup.outputs.release-version }}
|
||||
PACKAGE_VERSION: ${{ needs.setup.outputs.release_version }}
|
||||
run: |
|
||||
mv browser-source.zip browser-source-$PACKAGE_VERSION.zip
|
||||
mv dist-chrome.zip dist-chrome-$PACKAGE_VERSION.zip
|
||||
@@ -130,14 +130,14 @@ jobs:
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0
|
||||
with:
|
||||
artifacts: 'browser-source-${{ needs.setup.outputs.release-version }}.zip,
|
||||
dist-chrome-${{ needs.setup.outputs.release-version }}.zip,
|
||||
dist-opera-${{ needs.setup.outputs.release-version }}.zip,
|
||||
dist-firefox-${{ needs.setup.outputs.release-version }}.zip,
|
||||
dist-edge-${{ needs.setup.outputs.release-version }}.zip'
|
||||
artifacts: 'browser-source-${{ needs.setup.outputs.release_version }}.zip,
|
||||
dist-chrome-${{ needs.setup.outputs.release_version }}.zip,
|
||||
dist-opera-${{ needs.setup.outputs.release_version }}.zip,
|
||||
dist-firefox-${{ needs.setup.outputs.release_version }}.zip,
|
||||
dist-edge-${{ needs.setup.outputs.release_version }}.zip'
|
||||
commit: ${{ github.sha }}
|
||||
tag: "browser-v${{ needs.setup.outputs.release-version }}"
|
||||
name: "Browser v${{ needs.setup.outputs.release-version }}"
|
||||
tag: "browser-v${{ needs.setup.outputs.release_version }}"
|
||||
name: "Browser v${{ needs.setup.outputs.release_version }}"
|
||||
body: "<insert release notes here>"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
draft: true
|
||||
|
||||
6
.github/workflows/release-cli.yml
vendored
6
.github/workflows/release-cli.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
name: Setup
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
release-version: ${{ steps.version.outputs.version }}
|
||||
release_version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
|
||||
- name: Check Release Version
|
||||
id: version
|
||||
uses: bitwarden/gh-actions/release-version-check@main
|
||||
uses: bitwarden/gh-actions/release_version-check@main
|
||||
with:
|
||||
release-type: ${{ inputs.release_type }}
|
||||
project-type: ts
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||
uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0
|
||||
env:
|
||||
PKG_VERSION: ${{ needs.setup.outputs.release-version }}
|
||||
PKG_VERSION: ${{ needs.setup.outputs.release_version }}
|
||||
with:
|
||||
artifacts: "apps/cli/bw-oss-windows-${{ env.PKG_VERSION }}.zip,
|
||||
apps/cli/bw-oss-windows-sha256-${{ env.PKG_VERSION }}.txt,
|
||||
|
||||
50
.github/workflows/release-desktop-beta.yml
vendored
50
.github/workflows/release-desktop-beta.yml
vendored
@@ -16,9 +16,9 @@ jobs:
|
||||
name: Setup
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
release-version: ${{ steps.version.outputs.version }}
|
||||
release-channel: ${{ steps.release-channel.outputs.channel }}
|
||||
branch-name: ${{ steps.branch.outputs.branch-name }}
|
||||
release_version: ${{ steps.version.outputs.version }}
|
||||
release_channel: ${{ steps.release_channel.outputs.channel }}
|
||||
branch_name: ${{ steps.branch.outputs.branch_name }}
|
||||
build_number: ${{ steps.increment-version.outputs.build_number }}
|
||||
node_version: ${{ steps.retrieve-node-version.outputs.node_version }}
|
||||
steps:
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
|
||||
- name: Check Release Version
|
||||
id: version
|
||||
uses: bitwarden/gh-actions/release-version-check@main
|
||||
uses: bitwarden/gh-actions/release_version-check@main
|
||||
with:
|
||||
release-type: 'Initial Release'
|
||||
project-type: ts
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
echo "build_number=$BUILD_NUMBER" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get Version Channel
|
||||
id: release-channel
|
||||
id: release_channel
|
||||
run: |
|
||||
case "${{ steps.version.outputs.version }}" in
|
||||
*"alpha"*)
|
||||
@@ -102,7 +102,7 @@ jobs:
|
||||
|
||||
git push -u origin $branch_name
|
||||
|
||||
echo "branch-name=$branch_name" >> $GITHUB_OUTPUT
|
||||
echo "branch_name=$branch_name" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get Node Version
|
||||
id: retrieve-node-version
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
needs: setup
|
||||
env:
|
||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.release-version }}
|
||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.release_version }}
|
||||
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
|
||||
NODE_OPTIONS: --max_old_space_size=4096
|
||||
defaults:
|
||||
@@ -126,7 +126,7 @@ jobs:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ needs.setup.outputs.branch-name }}
|
||||
ref: ${{ needs.setup.outputs.branch_name }}
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
@@ -195,8 +195,8 @@ jobs:
|
||||
- name: Upload auto-update artifact
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
with:
|
||||
name: ${{ needs.setup.outputs.release-channel }}-linux.yml
|
||||
path: apps/desktop/dist/${{ needs.setup.outputs.release-channel }}-linux.yml
|
||||
name: ${{ needs.setup.outputs.release_channel }}-linux.yml
|
||||
path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-linux.yml
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
@@ -209,14 +209,14 @@ jobs:
|
||||
shell: pwsh
|
||||
working-directory: apps/desktop
|
||||
env:
|
||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.release-version }}
|
||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.release_version }}
|
||||
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
|
||||
NODE_OPTIONS: --max_old_space_size=4096
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ needs.setup.outputs.branch-name }}
|
||||
ref: ${{ needs.setup.outputs.branch_name }}
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
@@ -385,8 +385,8 @@ jobs:
|
||||
- name: Upload auto-update artifact
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
with:
|
||||
name: ${{ needs.setup.outputs.release-channel }}.yml
|
||||
path: apps/desktop/dist/nsis-web/${{ needs.setup.outputs.release-channel }}.yml
|
||||
name: ${{ needs.setup.outputs.release_channel }}.yml
|
||||
path: apps/desktop/dist/nsis-web/${{ needs.setup.outputs.release_channel }}.yml
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
@@ -395,7 +395,7 @@ jobs:
|
||||
runs-on: macos-13
|
||||
needs: setup
|
||||
env:
|
||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.release-version }}
|
||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.release_version }}
|
||||
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
|
||||
NODE_OPTIONS: --max_old_space_size=4096
|
||||
defaults:
|
||||
@@ -405,7 +405,7 @@ jobs:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ needs.setup.outputs.branch-name }}
|
||||
ref: ${{ needs.setup.outputs.branch_name }}
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
@@ -529,7 +529,7 @@ jobs:
|
||||
- setup
|
||||
- macos-build
|
||||
env:
|
||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.release-version }}
|
||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.release_version }}
|
||||
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
|
||||
NODE_OPTIONS: --max_old_space_size=4096
|
||||
defaults:
|
||||
@@ -539,7 +539,7 @@ jobs:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ needs.setup.outputs.branch-name }}
|
||||
ref: ${{ needs.setup.outputs.branch_name }}
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
@@ -730,8 +730,8 @@ jobs:
|
||||
- name: Upload auto-update artifact
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
with:
|
||||
name: ${{ needs.setup.outputs.release-channel }}-mac.yml
|
||||
path: apps/desktop/dist/${{ needs.setup.outputs.release-channel }}-mac.yml
|
||||
name: ${{ needs.setup.outputs.release_channel }}-mac.yml
|
||||
path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-mac.yml
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
@@ -742,7 +742,7 @@ jobs:
|
||||
- setup
|
||||
- macos-build
|
||||
env:
|
||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.release-version }}
|
||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.release_version }}
|
||||
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
|
||||
NODE_OPTIONS: --max_old_space_size=4096
|
||||
defaults:
|
||||
@@ -752,7 +752,7 @@ jobs:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ needs.setup.outputs.branch-name }}
|
||||
ref: ${{ needs.setup.outputs.branch_name }}
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
@@ -939,7 +939,7 @@ jobs:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
initial-status: 'in_progress'
|
||||
environment: 'Desktop - Beta'
|
||||
description: 'Deployment ${{ needs.setup.outputs.release-version }} to channel ${{ needs.setup.outputs.release-channel }} from branch ${{ needs.setup.outputs.branch-name }}'
|
||||
description: 'Deployment ${{ needs.setup.outputs.release_version }} to channel ${{ needs.setup.outputs.release_channel }} from branch ${{ needs.setup.outputs.branch_name }}'
|
||||
task: release
|
||||
|
||||
- name: Login to Azure
|
||||
@@ -963,7 +963,7 @@ jobs:
|
||||
|
||||
- name: Rename .pkg to .pkg.archive
|
||||
env:
|
||||
PKG_VERSION: ${{ needs.setup.outputs.release-version }}
|
||||
PKG_VERSION: ${{ needs.setup.outputs.release_version }}
|
||||
working-directory: apps/desktop/artifacts
|
||||
run: mv Bitwarden-${{ env.PKG_VERSION }}-universal.pkg Bitwarden-${{ env.PKG_VERSION }}-universal.pkg.archive
|
||||
|
||||
@@ -1020,5 +1020,5 @@ jobs:
|
||||
git config --global url."https://".insteadOf ssh://
|
||||
- name: Remove branch
|
||||
env:
|
||||
BRANCH: ${{ needs.setup.outputs.branch-name }}
|
||||
BRANCH: ${{ needs.setup.outputs.branch_name }}
|
||||
run: git push origin --delete $BRANCH
|
||||
|
||||
12
.github/workflows/release-desktop.yml
vendored
12
.github/workflows/release-desktop.yml
vendored
@@ -22,8 +22,8 @@ jobs:
|
||||
name: Setup
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
release-version: ${{ steps.version.outputs.version }}
|
||||
release-channel: ${{ steps.release-channel.outputs.channel }}
|
||||
release_version: ${{ steps.version.outputs.version }}
|
||||
release_channel: ${{ steps.release_channel.outputs.channel }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
|
||||
- name: Check Release Version
|
||||
id: version
|
||||
uses: bitwarden/gh-actions/release-version-check@main
|
||||
uses: bitwarden/gh-actions/release_version-check@main
|
||||
with:
|
||||
release-type: ${{ inputs.release_type }}
|
||||
project-type: ts
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
monorepo-project: desktop
|
||||
|
||||
- name: Get Version Channel
|
||||
id: release-channel
|
||||
id: release_channel
|
||||
run: |
|
||||
case "${{ steps.version.outputs.version }}" in
|
||||
*"alpha"*)
|
||||
@@ -97,10 +97,10 @@ jobs:
|
||||
|
||||
- name: Create Release
|
||||
uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0
|
||||
if: ${{ steps.release-channel.outputs.channel == 'latest' && github.event.inputs.release_type != 'Dry Run' }}
|
||||
if: ${{ steps.release_channel.outputs.channel == 'latest' && github.event.inputs.release_type != 'Dry Run' }}
|
||||
env:
|
||||
PKG_VERSION: ${{ steps.version.outputs.version }}
|
||||
RELEASE_CHANNEL: ${{ steps.release-channel.outputs.channel }}
|
||||
RELEASE_CHANNEL: ${{ steps.release_channel.outputs.channel }}
|
||||
with:
|
||||
artifacts: "apps/desktop/artifacts/Bitwarden-${{ env.PKG_VERSION }}-amd64.deb,
|
||||
apps/desktop/artifacts/Bitwarden-${{ env.PKG_VERSION }}-x86_64.rpm,
|
||||
|
||||
@@ -2804,6 +2804,20 @@
|
||||
"error": {
|
||||
"message": "Error"
|
||||
},
|
||||
"decryptionError": {
|
||||
"message": "Decryption error"
|
||||
},
|
||||
"couldNotDecryptVaultItemsBelow": {
|
||||
"message": "Bitwarden could not decrypt the vault item(s) listed below."
|
||||
},
|
||||
"contactCSToAvoidDataLossPart1": {
|
||||
"message": "Contact customer success",
|
||||
"description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'"
|
||||
},
|
||||
"contactCSToAvoidDataLossPart2": {
|
||||
"message": "to avoid additional data loss.",
|
||||
"description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'"
|
||||
},
|
||||
"generateUsername": {
|
||||
"message": "Generate username"
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
size="small"
|
||||
[attr.aria-label]="'moreOptionsLabel' | i18n: cipher.name"
|
||||
[title]="'moreOptionsTitle' | i18n: cipher.name"
|
||||
[disabled]="cipher.decryptionFailure"
|
||||
[bitMenuTriggerFor]="moreOptions"
|
||||
></button>
|
||||
<bit-menu #moreOptions>
|
||||
|
||||
@@ -18,19 +18,25 @@ import { map } from "rxjs";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
BadgeModule,
|
||||
ButtonModule,
|
||||
CompactModeService,
|
||||
DialogService,
|
||||
IconButtonModule,
|
||||
ItemModule,
|
||||
SectionComponent,
|
||||
SectionHeaderComponent,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
import { OrgIconDirective, PasswordRepromptService } from "@bitwarden/vault";
|
||||
import {
|
||||
DecryptionFailureDialogComponent,
|
||||
OrgIconDirective,
|
||||
PasswordRepromptService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { BrowserApi } from "../../../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";
|
||||
@@ -55,6 +61,7 @@ import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options
|
||||
ItemMoreOptionsComponent,
|
||||
OrgIconDirective,
|
||||
ScrollingModule,
|
||||
DecryptionFailureDialogComponent,
|
||||
],
|
||||
selector: "app-vault-list-items-container",
|
||||
templateUrl: "vault-list-items-container.component.html",
|
||||
@@ -158,6 +165,7 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
private cipherService: CipherService,
|
||||
private router: Router,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private dialogService: DialogService,
|
||||
) {}
|
||||
|
||||
async ngAfterViewInit() {
|
||||
@@ -209,6 +217,13 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
this.viewCipherTimeout = window.setTimeout(
|
||||
async () => {
|
||||
try {
|
||||
if (cipher.decryptionFailure) {
|
||||
DecryptionFailureDialogComponent.open(this.dialogService, {
|
||||
cipherIds: [cipher.id as CipherId],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const repromptPassed = await this.passwordRepromptService.passwordRepromptCheck(cipher);
|
||||
if (!repromptPassed) {
|
||||
return;
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { ScrollingModule } from "@angular/cdk/scrolling";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Component, DestroyRef, OnDestroy, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { RouterLink } from "@angular/router";
|
||||
import { combineLatest, Observable, shareReplay, switchMap } from "rxjs";
|
||||
import { filter, map, take } from "rxjs/operators";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { ButtonModule, Icons, NoItemsModule } from "@bitwarden/components";
|
||||
import { VaultIcons } from "@bitwarden/vault";
|
||||
import { ButtonModule, DialogService, Icons, NoItemsModule } from "@bitwarden/components";
|
||||
import { DecryptionFailureDialogComponent, VaultIcons } from "@bitwarden/vault";
|
||||
|
||||
import { CurrentAccountComponent } from "../../../../auth/popup/account-switching/current-account.component";
|
||||
import { PopOutComponent } from "../../../../platform/popup/components/pop-out.component";
|
||||
@@ -52,6 +54,7 @@ enum VaultState {
|
||||
NewItemDropdownV2Component,
|
||||
ScrollingModule,
|
||||
VaultHeaderV2Component,
|
||||
DecryptionFailureDialogComponent,
|
||||
],
|
||||
providers: [VaultUiOnboardingService],
|
||||
})
|
||||
@@ -89,6 +92,9 @@ export class VaultV2Component implements OnInit, OnDestroy {
|
||||
private vaultPopupItemsService: VaultPopupItemsService,
|
||||
private vaultPopupListFiltersService: VaultPopupListFiltersService,
|
||||
private vaultUiOnboardingService: VaultUiOnboardingService,
|
||||
private destroyRef: DestroyRef,
|
||||
private cipherService: CipherService,
|
||||
private dialogService: DialogService,
|
||||
) {
|
||||
combineLatest([
|
||||
this.vaultPopupItemsService.emptyVault$,
|
||||
@@ -116,6 +122,19 @@ export class VaultV2Component implements OnInit, OnDestroy {
|
||||
|
||||
async ngOnInit() {
|
||||
await this.vaultUiOnboardingService.showOnboardingDialog();
|
||||
|
||||
this.cipherService.failedToDecryptCiphers$
|
||||
.pipe(
|
||||
map((ciphers) => ciphers.filter((c) => !c.isDeleted)),
|
||||
filter((ciphers) => ciphers.length > 0),
|
||||
take(1),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
.subscribe((ciphers) => {
|
||||
DecryptionFailureDialogComponent.open(this.dialogService, {
|
||||
cipherIds: ciphers.map((c) => c.id as CipherId),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {}
|
||||
|
||||
@@ -58,6 +58,7 @@ describe("VaultPopupItemsService", () => {
|
||||
cipherServiceMock.getAllDecrypted.mockResolvedValue(cipherList);
|
||||
cipherServiceMock.ciphers$ = new BehaviorSubject(null);
|
||||
cipherServiceMock.localData$ = new BehaviorSubject(null);
|
||||
cipherServiceMock.failedToDecryptCiphers$ = new BehaviorSubject([]);
|
||||
searchService.searchCiphers.mockImplementation(async (_, __, ciphers) => ciphers);
|
||||
cipherServiceMock.filterCiphersForUrl.mockImplementation(async (ciphers) =>
|
||||
ciphers.filter((c) => ["0", "1"].includes(c.id)),
|
||||
@@ -294,21 +295,6 @@ describe("VaultPopupItemsService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should sort by last used then by name by default", (done) => {
|
||||
service.remainingCiphers$.subscribe(() => {
|
||||
expect(cipherServiceMock.getLocaleSortingFunction).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should NOT sort by last used then by name when search text is applied", (done) => {
|
||||
service.applyFilter("Login");
|
||||
service.remainingCiphers$.subscribe(() => {
|
||||
expect(cipherServiceMock.getLocaleSortingFunction).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should filter remainingCiphers$ down to search term", (done) => {
|
||||
const cipherList = Object.values(allCiphers);
|
||||
const searchText = "Login";
|
||||
|
||||
@@ -90,6 +90,8 @@ export class VaultPopupItemsService {
|
||||
tap(() => this._ciphersLoading$.next()),
|
||||
waitUntilSync(this.syncService),
|
||||
switchMap(() => Utils.asyncToObservable(() => this.cipherService.getAllDecrypted())),
|
||||
withLatestFrom(this.cipherService.failedToDecryptCiphers$),
|
||||
map(([ciphers, failedToDecryptCiphers]) => [...failedToDecryptCiphers, ...ciphers]),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
@@ -190,11 +192,6 @@ export class VaultPopupItemsService {
|
||||
(cipher) => !autoFillCiphers.includes(cipher) && !favoriteCiphers.includes(cipher),
|
||||
),
|
||||
),
|
||||
withLatestFrom(this._hasSearchText$),
|
||||
map(([ciphers, hasSearchText]) =>
|
||||
// Do not sort alphabetically when there is search text, default to the search service scoring
|
||||
hasSearchText ? ciphers : ciphers.sort(this.cipherService.getLocaleSortingFunction()),
|
||||
),
|
||||
shareReplay({ refCount: false, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
|
||||
@@ -27,7 +27,12 @@
|
||||
[bitMenuTriggerFor]="moreOptions"
|
||||
></button>
|
||||
<bit-menu #moreOptions>
|
||||
<button type="button" bitMenuItem (click)="restore(cipher)">
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="restore(cipher)"
|
||||
*ngIf="!cipher.decryptionFailure"
|
||||
>
|
||||
{{ "restore" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem *appCanDeleteCipher="cipher" (click)="delete(cipher)">
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Router } from "@angular/router";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { CipherId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
@@ -19,7 +20,11 @@ import {
|
||||
ToastService,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
import { CanDeleteCipherDirective, PasswordRepromptService } from "@bitwarden/vault";
|
||||
import {
|
||||
CanDeleteCipherDirective,
|
||||
DecryptionFailureDialogComponent,
|
||||
PasswordRepromptService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
@Component({
|
||||
selector: "app-trash-list-items-container",
|
||||
@@ -35,6 +40,7 @@ import { CanDeleteCipherDirective, PasswordRepromptService } from "@bitwarden/va
|
||||
MenuModule,
|
||||
IconButtonModule,
|
||||
TypographyModule,
|
||||
DecryptionFailureDialogComponent,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
@@ -105,6 +111,13 @@ export class TrashListItemsContainerComponent {
|
||||
}
|
||||
|
||||
async onViewCipher(cipher: CipherView) {
|
||||
if (cipher.decryptionFailure) {
|
||||
DecryptionFailureDialogComponent.open(this.dialogService, {
|
||||
cipherIds: [cipher.id as CipherId],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const repromptPassed = await this.passwordRepromptService.passwordRepromptCheck(cipher);
|
||||
if (!repromptPassed) {
|
||||
return;
|
||||
|
||||
@@ -7,7 +7,8 @@ import { NgModule } from "@angular/core";
|
||||
|
||||
import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-count.pipe";
|
||||
import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe";
|
||||
import { DialogModule, CalloutModule } from "@bitwarden/components";
|
||||
import { CalloutModule, DialogModule } from "@bitwarden/components";
|
||||
import { DecryptionFailureDialogComponent } from "@bitwarden/vault";
|
||||
|
||||
import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component";
|
||||
import { DeleteAccountComponent } from "../auth/delete-account.component";
|
||||
@@ -61,6 +62,7 @@ import { SendComponent } from "./tools/send/send.component";
|
||||
CalloutModule,
|
||||
DeleteAccountComponent,
|
||||
UserVerificationComponent,
|
||||
DecryptionFailureDialogComponent,
|
||||
],
|
||||
declarations: [
|
||||
AccessibilityCookieComponent,
|
||||
|
||||
@@ -249,6 +249,20 @@
|
||||
"error": {
|
||||
"message": "Error"
|
||||
},
|
||||
"decryptionError": {
|
||||
"message": "Decryption error"
|
||||
},
|
||||
"couldNotDecryptVaultItemsBelow": {
|
||||
"message": "Bitwarden could not decrypt the vault item(s) listed below."
|
||||
},
|
||||
"contactCSToAvoidDataLossPart1": {
|
||||
"message": "Contact customer success",
|
||||
"description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'"
|
||||
},
|
||||
"contactCSToAvoidDataLossPart2": {
|
||||
"message": "to avoid additional data loss.",
|
||||
"description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'"
|
||||
},
|
||||
"january": {
|
||||
"message": "January"
|
||||
},
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
ViewContainerRef,
|
||||
} from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { Subject, takeUntil, switchMap } from "rxjs";
|
||||
import { first } from "rxjs/operators";
|
||||
import { combineLatest, firstValueFrom, Subject, takeUntil, switchMap } from "rxjs";
|
||||
import { filter, first, map, take } from "rxjs/operators";
|
||||
|
||||
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
@@ -28,13 +28,15 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { CipherId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
import { DecryptionFailureDialogComponent, PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { SearchBarService } from "../../../app/layout/search/search-bar.service";
|
||||
import { GeneratorComponent } from "../../../app/tools/generator.component";
|
||||
@@ -113,6 +115,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private configService: ConfigService,
|
||||
private accountService: AccountService,
|
||||
private cipherService: CipherService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -238,6 +241,25 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
notificationId: authRequest.id,
|
||||
});
|
||||
}
|
||||
|
||||
// Store a reference to the current active account during page init
|
||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
|
||||
// Combine with the activeAccount$ to ensure we only show the dialog for the current account from ngOnInit.
|
||||
// The account switching process updates the cipherService before Vault is destroyed and would cause duplicate emissions
|
||||
combineLatest([this.accountService.activeAccount$, this.cipherService.failedToDecryptCiphers$])
|
||||
.pipe(
|
||||
filter(([account]) => account.id === activeAccount.id),
|
||||
map(([_, ciphers]) => ciphers.filter((c) => !c.isDeleted)),
|
||||
filter((ciphers) => ciphers.length > 0),
|
||||
take(1),
|
||||
takeUntil(this.componentIsDestroyed$),
|
||||
)
|
||||
.subscribe((ciphers) => {
|
||||
DecryptionFailureDialogComponent.open(this.dialogService, {
|
||||
cipherIds: ciphers.map((c) => c.id as CipherId),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -302,6 +324,12 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
if (cipher.decryptionFailure) {
|
||||
invokeMenu(menu);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cipher.isDeleted) {
|
||||
menu.push({
|
||||
label: this.i18nService.t("edit"),
|
||||
|
||||
@@ -638,33 +638,35 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer" *ngIf="cipher">
|
||||
<button
|
||||
type="button"
|
||||
class="primary"
|
||||
(click)="edit()"
|
||||
appA11yTitle="{{ 'edit' | i18n }}"
|
||||
*ngIf="!cipher.isDeleted"
|
||||
>
|
||||
<i class="bwi bwi-pencil bwi-fw bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="primary"
|
||||
(click)="restore()"
|
||||
appA11yTitle="{{ 'restore' | i18n }}"
|
||||
*ngIf="cipher.isDeleted"
|
||||
>
|
||||
<i class="bwi bwi-undo bwi-fw bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="primary"
|
||||
*ngIf="!cipher?.organizationId && !cipher.isDeleted"
|
||||
(click)="clone()"
|
||||
appA11yTitle="{{ 'clone' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-files bwi-fw bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ng-container *ngIf="!cipher.decryptionFailure">
|
||||
<button
|
||||
type="button"
|
||||
class="primary"
|
||||
(click)="edit()"
|
||||
appA11yTitle="{{ 'edit' | i18n }}"
|
||||
*ngIf="!cipher.isDeleted"
|
||||
>
|
||||
<i class="bwi bwi-pencil bwi-fw bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="primary"
|
||||
(click)="restore()"
|
||||
appA11yTitle="{{ 'restore' | i18n }}"
|
||||
*ngIf="cipher.isDeleted"
|
||||
>
|
||||
<i class="bwi bwi-undo bwi-fw bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="primary"
|
||||
*ngIf="!cipher?.organizationId && !cipher.isDeleted"
|
||||
(click)="clone()"
|
||||
appA11yTitle="{{ 'clone' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-files bwi-fw bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
</ng-container>
|
||||
<div class="right" *ngIf="canDeleteCipher$ | async">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -25,6 +25,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { CipherId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
@@ -32,7 +33,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
import { DecryptionFailureDialogComponent, PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
const BroadcasterSubscriptionId = "ViewComponent";
|
||||
|
||||
@@ -98,6 +99,7 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro
|
||||
}
|
||||
ngOnInit() {
|
||||
super.ngOnInit();
|
||||
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
|
||||
this.ngZone.run(() => {
|
||||
switch (message.command) {
|
||||
@@ -117,6 +119,13 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro
|
||||
|
||||
async ngOnChanges() {
|
||||
await super.load();
|
||||
|
||||
if (this.cipher.decryptionFailure) {
|
||||
DecryptionFailureDialogComponent.open(this.dialogService, {
|
||||
cipherIds: [this.cipherId as CipherId],
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
viewHistory() {
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
CipherFormGenerationService,
|
||||
CipherFormModule,
|
||||
CipherViewComponent,
|
||||
DecryptionFailureDialogComponent,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { SharedModule } from "../../../shared/shared.module";
|
||||
@@ -114,6 +115,7 @@ export enum VaultItemDialogResult {
|
||||
CipherAttachmentsComponent,
|
||||
AsyncActionsModule,
|
||||
ItemModule,
|
||||
DecryptionFailureDialogComponent,
|
||||
],
|
||||
providers: [
|
||||
{ provide: PremiumUpgradePromptService, useClass: WebVaultPremiumUpgradePromptService },
|
||||
@@ -252,6 +254,14 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
|
||||
this.cipher = await this.getDecryptedCipherView(this.formConfig);
|
||||
|
||||
if (this.cipher) {
|
||||
if (this.cipher.decryptionFailure) {
|
||||
this.dialogService.open(DecryptionFailureDialogComponent, {
|
||||
data: { cipherIds: [this.cipher.id] },
|
||||
});
|
||||
this.dialogRef.close();
|
||||
return;
|
||||
}
|
||||
|
||||
this.collections = this.formConfig.collections.filter((c) =>
|
||||
this.cipher.collectionIds?.includes(c.id),
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
type="checkbox"
|
||||
bitCheckbox
|
||||
appStopProp
|
||||
[disabled]="disabled"
|
||||
[disabled]="disabled || cipher.decryptionFailure"
|
||||
[checked]="checked"
|
||||
(change)="$event ? this.checkedToggled.next() : null"
|
||||
[attr.aria-label]="'vaultItemSelect' | i18n"
|
||||
@@ -20,7 +20,7 @@
|
||||
class="tw-overflow-hidden tw-text-ellipsis tw-text-start tw-leading-snug"
|
||||
[disabled]="disabled"
|
||||
[routerLink]="[]"
|
||||
[queryParams]="{ itemId: cipher.id, action: extensionRefreshEnabled ? 'view' : null }"
|
||||
[queryParams]="{ itemId: cipher.id, action: clickAction }"
|
||||
queryParamsHandling="merge"
|
||||
[replaceUrl]="extensionRefreshEnabled"
|
||||
title="{{ 'editItemWithName' | i18n: cipher.name }}"
|
||||
@@ -76,6 +76,25 @@
|
||||
</td>
|
||||
<td bitCell [ngClass]="RowHeightClass" class="tw-text-right">
|
||||
<button
|
||||
*ngIf="cipher.decryptionFailure"
|
||||
[disabled]="disabled || !canManageCollection"
|
||||
[bitMenuTriggerFor]="corruptedCipherOptions"
|
||||
size="small"
|
||||
bitIconButton="bwi-ellipsis-v"
|
||||
type="button"
|
||||
appA11yTitle="{{ 'options' | i18n }}"
|
||||
appStopProp
|
||||
></button>
|
||||
<bit-menu #corruptedCipherOptions>
|
||||
<button bitMenuItem *ngIf="canManageCollection" (click)="deleteCipher()" type="button">
|
||||
<span class="tw-text-danger">
|
||||
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
|
||||
{{ (cipher.isDeleted ? "permanentlyDelete" : "delete") | i18n }}
|
||||
</span>
|
||||
</button>
|
||||
</bit-menu>
|
||||
<button
|
||||
*ngIf="!cipher.decryptionFailure"
|
||||
[disabled]="disabled || disableMenu"
|
||||
[bitMenuTriggerFor]="cipherOptions"
|
||||
size="small"
|
||||
|
||||
@@ -78,6 +78,13 @@ export class VaultCipherRowComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
protected get clickAction() {
|
||||
if (this.cipher.decryptionFailure) {
|
||||
return "showFailedToDecrypt";
|
||||
}
|
||||
return this.extensionRefreshEnabled ? "view" : null;
|
||||
}
|
||||
|
||||
protected get showTotpCopyButton() {
|
||||
return (
|
||||
(this.cipher.login?.hasTotp ?? false) &&
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
map,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
take,
|
||||
takeUntil,
|
||||
tap,
|
||||
} from "rxjs/operators";
|
||||
@@ -75,6 +76,7 @@ import { DialogService, Icons, ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
CipherFormConfig,
|
||||
CollectionAssignmentResult,
|
||||
DecryptionFailureDialogComponent,
|
||||
DefaultCipherFormConfigService,
|
||||
PasswordRepromptService,
|
||||
} from "@bitwarden/vault";
|
||||
@@ -144,6 +146,7 @@ const SearchTextDebounceInterval = 200;
|
||||
VaultFilterModule,
|
||||
VaultItemsModule,
|
||||
SharedModule,
|
||||
DecryptionFailureDialogComponent,
|
||||
],
|
||||
providers: [
|
||||
RoutedVaultFilterService,
|
||||
@@ -359,13 +362,16 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
]).pipe(
|
||||
filter(([ciphers, filter]) => ciphers != undefined && filter != undefined),
|
||||
concatMap(async ([ciphers, filter, searchText]) => {
|
||||
const failedCiphers = await firstValueFrom(this.cipherService.failedToDecryptCiphers$);
|
||||
const filterFunction = createFilterFunction(filter);
|
||||
// Append any failed to decrypt ciphers to the top of the cipher list
|
||||
const allCiphers = [...failedCiphers, ...ciphers];
|
||||
|
||||
if (await this.searchService.isSearchable(searchText)) {
|
||||
return await this.searchService.searchCiphers(searchText, [filterFunction], ciphers);
|
||||
return await this.searchService.searchCiphers(searchText, [filterFunction], allCiphers);
|
||||
}
|
||||
|
||||
return ciphers.filter(filterFunction);
|
||||
return allCiphers.filter(filterFunction);
|
||||
}),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
@@ -436,6 +442,18 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
action = "view";
|
||||
}
|
||||
|
||||
if (action == "showFailedToDecrypt") {
|
||||
DecryptionFailureDialogComponent.open(this.dialogService, {
|
||||
cipherIds: [cipherId as CipherId],
|
||||
});
|
||||
await this.router.navigate([], {
|
||||
queryParams: { itemId: null, cipherId: null, action: null },
|
||||
queryParamsHandling: "merge",
|
||||
replaceUrl: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "view") {
|
||||
await this.viewCipherById(cipherId);
|
||||
} else {
|
||||
@@ -458,6 +476,20 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
firstSetup$
|
||||
.pipe(
|
||||
switchMap(() => this.cipherService.failedToDecryptCiphers$),
|
||||
map((ciphers) => ciphers.filter((c) => !c.isDeleted)),
|
||||
filter((ciphers) => ciphers.length > 0),
|
||||
take(1),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((ciphers) => {
|
||||
DecryptionFailureDialogComponent.open(this.dialogService, {
|
||||
cipherIds: ciphers.map((c) => c.id as CipherId),
|
||||
});
|
||||
});
|
||||
|
||||
this.unpaidSubscriptionDialog$.pipe(takeUntil(this.destroy$)).subscribe();
|
||||
|
||||
firstSetup$
|
||||
|
||||
@@ -38,9 +38,9 @@ import {
|
||||
import {
|
||||
CollectionAdminService,
|
||||
CollectionAdminView,
|
||||
Unassigned,
|
||||
CollectionService,
|
||||
CollectionView,
|
||||
Unassigned,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
@@ -71,16 +71,17 @@ import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||
import {
|
||||
BannerModule,
|
||||
DialogService,
|
||||
Icons,
|
||||
NoItemsModule,
|
||||
ToastService,
|
||||
BannerModule,
|
||||
} from "@bitwarden/components";
|
||||
import {
|
||||
CipherFormConfig,
|
||||
CipherFormConfigService,
|
||||
CollectionAssignmentResult,
|
||||
DecryptionFailureDialogComponent,
|
||||
PasswordRepromptService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
@@ -134,6 +135,7 @@ import {
|
||||
import { CollectionAccessRestrictedComponent } from "./collection-access-restricted.component";
|
||||
import { AdminConsoleCipherFormConfigService } from "./services/admin-console-cipher-form-config.service";
|
||||
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
|
||||
|
||||
const BroadcasterSubscriptionId = "OrgVaultComponent";
|
||||
const SearchTextDebounceInterval = 200;
|
||||
|
||||
@@ -549,11 +551,24 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
|
||||
if (cipher) {
|
||||
let action = qParams.action;
|
||||
|
||||
// Default to "view" if extension refresh is enabled
|
||||
if (action == null && this.extensionRefreshEnabled) {
|
||||
action = "view";
|
||||
}
|
||||
|
||||
if (action == "showFailedToDecrypt") {
|
||||
DecryptionFailureDialogComponent.open(this.dialogService, {
|
||||
cipherIds: [cipherId as CipherId],
|
||||
});
|
||||
await this.router.navigate([], {
|
||||
queryParams: { itemId: null, cipherId: null, action: null },
|
||||
queryParamsHandling: "merge",
|
||||
replaceUrl: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "view") {
|
||||
await this.viewCipherById(cipher);
|
||||
} else {
|
||||
|
||||
@@ -5676,6 +5676,20 @@
|
||||
"error": {
|
||||
"message": "Error"
|
||||
},
|
||||
"decryptionError": {
|
||||
"message": "Decryption error"
|
||||
},
|
||||
"couldNotDecryptVaultItemsBelow": {
|
||||
"message": "Bitwarden could not decrypt the vault item(s) listed below."
|
||||
},
|
||||
"contactCSToAvoidDataLossPart1": {
|
||||
"message": "Contact customer success",
|
||||
"description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'"
|
||||
},
|
||||
"contactCSToAvoidDataLossPart2": {
|
||||
"message": "to avoid additional data loss.",
|
||||
"description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'"
|
||||
},
|
||||
"accountRecoveryManageUsers": {
|
||||
"message": "Manage users must also be granted with the manage account recovery permission"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { BehaviorSubject, Subject, from, switchMap, takeUntil } from "rxjs";
|
||||
import { BehaviorSubject, firstValueFrom, from, Subject, switchMap, takeUntil } from "rxjs";
|
||||
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
@@ -118,6 +118,12 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
|
||||
|
||||
protected async doSearch(indexedCiphers?: CipherView[]) {
|
||||
indexedCiphers = indexedCiphers ?? (await this.cipherService.getAllDecrypted());
|
||||
|
||||
const failedCiphers = await firstValueFrom(this.cipherService.failedToDecryptCiphers$);
|
||||
if (failedCiphers != null && failedCiphers.length > 0) {
|
||||
indexedCiphers = [...failedCiphers, ...indexedCiphers];
|
||||
}
|
||||
|
||||
this.ciphers = await this.searchService.searchCiphers(
|
||||
this.searchText,
|
||||
[this.filter, this.deletedFilter],
|
||||
|
||||
@@ -26,6 +26,12 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
|
||||
* An observable monitoring the add/edit cipher info saved to memory.
|
||||
*/
|
||||
addEditCipherInfo$: Observable<AddEditCipherInfo>;
|
||||
/**
|
||||
* Observable that emits an array of cipherViews that failed to decrypt. Does not emit until decryption has completed.
|
||||
*
|
||||
* An empty array indicates that all ciphers were successfully decrypted.
|
||||
*/
|
||||
failedToDecryptCiphers$: Observable<CipherView[]>;
|
||||
clearCache: (userId?: string) => Promise<void>;
|
||||
encrypt: (
|
||||
model: CipherView,
|
||||
|
||||
@@ -136,7 +136,13 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
|
||||
|
||||
if (this.key != null) {
|
||||
const encryptService = Utils.getContainerService().getEncryptService();
|
||||
encKey = new SymmetricCryptoKey(await encryptService.decryptToBytes(this.key, encKey));
|
||||
const keyBytes = await encryptService.decryptToBytes(this.key, encKey);
|
||||
if (keyBytes == null) {
|
||||
model.name = "[error: cannot decrypt]";
|
||||
model.decryptionFailure = true;
|
||||
return model;
|
||||
}
|
||||
encKey = new SymmetricCryptoKey(keyBytes);
|
||||
bypassValidation = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,11 @@ export class CipherView implements View, InitializerMetadata {
|
||||
deletedDate: Date = null;
|
||||
reprompt: CipherRepromptType = CipherRepromptType.None;
|
||||
|
||||
/**
|
||||
* Flag to indicate if the cipher decryption failed.
|
||||
*/
|
||||
decryptionFailure = false;
|
||||
|
||||
constructor(c?: Cipher) {
|
||||
if (!c) {
|
||||
return;
|
||||
|
||||
@@ -359,6 +359,7 @@ describe("Cipher Service", () => {
|
||||
const originalUserKey = new SymmetricCryptoKey(new Uint8Array(32)) as UserKey;
|
||||
const newUserKey = new SymmetricCryptoKey(new Uint8Array(32)) as UserKey;
|
||||
let decryptedCiphers: BehaviorSubject<Record<CipherId, CipherView>>;
|
||||
let failedCiphers: BehaviorSubject<CipherView[]>;
|
||||
let encryptedKey: EncString;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -385,6 +386,7 @@ describe("Cipher Service", () => {
|
||||
Cipher2: cipher2,
|
||||
});
|
||||
cipherService.cipherViews$ = decryptedCiphers.pipe(map((ciphers) => Object.values(ciphers)));
|
||||
cipherService.failedToDecryptCiphers$ = failedCiphers = new BehaviorSubject<CipherView[]>([]);
|
||||
|
||||
encryptService.decryptToBytes.mockResolvedValue(new Uint8Array(32));
|
||||
encryptedKey = new EncString("Re-encrypted Cipher Key");
|
||||
@@ -413,5 +415,16 @@ describe("Cipher Service", () => {
|
||||
"New user key is required to rotate ciphers",
|
||||
);
|
||||
});
|
||||
|
||||
it("throws if the user has any failed to decrypt ciphers", async () => {
|
||||
const badCipher = new CipherView(cipherObj);
|
||||
badCipher.id = "Cipher 3";
|
||||
badCipher.organizationId = null;
|
||||
badCipher.decryptionFailure = true;
|
||||
failedCiphers.next([badCipher]);
|
||||
await expect(
|
||||
cipherService.getRotatedData(originalUserKey, newUserKey, mockUserId),
|
||||
).rejects.toThrow("Cannot rotate ciphers when decryption failures are present");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
map,
|
||||
merge,
|
||||
Observable,
|
||||
of,
|
||||
shareReplay,
|
||||
Subject,
|
||||
switchMap,
|
||||
@@ -79,6 +80,7 @@ import {
|
||||
ADD_EDIT_CIPHER_INFO_KEY,
|
||||
DECRYPTED_CIPHERS,
|
||||
ENCRYPTED_CIPHERS,
|
||||
FAILED_DECRYPTED_CIPHERS,
|
||||
LOCAL_DATA_KEY,
|
||||
} from "./key-state/ciphers.state";
|
||||
|
||||
@@ -109,9 +111,17 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
cipherViews$: Observable<CipherView[] | null>;
|
||||
addEditCipherInfo$: Observable<AddEditCipherInfo>;
|
||||
|
||||
/**
|
||||
* Observable that emits an array of cipherViews that failed to decrypt. Does not emit until decryption has completed.
|
||||
*
|
||||
* An empty array indicates that all ciphers were successfully decrypted.
|
||||
*/
|
||||
failedToDecryptCiphers$: Observable<CipherView[]>;
|
||||
|
||||
private localDataState: ActiveUserState<Record<CipherId, LocalData>>;
|
||||
private encryptedCiphersState: ActiveUserState<Record<CipherId, CipherData>>;
|
||||
private decryptedCiphersState: ActiveUserState<Record<CipherId, CipherView>>;
|
||||
private failedToDecryptCiphersState: ActiveUserState<CipherView[]>;
|
||||
private addEditCipherInfoState: ActiveUserState<AddEditCipherInfo>;
|
||||
|
||||
constructor(
|
||||
@@ -132,6 +142,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
this.localDataState = this.stateProvider.getActive(LOCAL_DATA_KEY);
|
||||
this.encryptedCiphersState = this.stateProvider.getActive(ENCRYPTED_CIPHERS);
|
||||
this.decryptedCiphersState = this.stateProvider.getActive(DECRYPTED_CIPHERS);
|
||||
this.failedToDecryptCiphersState = this.stateProvider.getActive(FAILED_DECRYPTED_CIPHERS);
|
||||
this.addEditCipherInfoState = this.stateProvider.getActive(ADD_EDIT_CIPHER_INFO_KEY);
|
||||
|
||||
this.localData$ = this.localDataState.state$.pipe(map((data) => data ?? {}));
|
||||
@@ -143,6 +154,13 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
switchMap(() => merge(this.forceCipherViews$, this.getAllDecrypted())),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
this.failedToDecryptCiphers$ = this.failedToDecryptCiphersState.state$.pipe(
|
||||
filter((ciphers) => ciphers != null),
|
||||
switchMap((ciphers) => merge(this.forceCipherViews$, of(ciphers))),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
this.addEditCipherInfo$ = this.addEditCipherInfoState.state$;
|
||||
}
|
||||
|
||||
@@ -162,6 +180,10 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
}
|
||||
}
|
||||
|
||||
async setFailedDecryptedCiphers(cipherViews: CipherView[], userId: UserId) {
|
||||
await this.stateProvider.setUserState(FAILED_DECRYPTED_CIPHERS, cipherViews, userId);
|
||||
}
|
||||
|
||||
private async setDecryptedCiphers(value: CipherView[], userId: UserId) {
|
||||
const cipherViews: { [id: string]: CipherView } = {};
|
||||
value?.forEach((c) => {
|
||||
@@ -378,7 +400,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
*/
|
||||
@sequentialize(() => "getAllDecrypted")
|
||||
async getAllDecrypted(): Promise<CipherView[]> {
|
||||
let decCiphers = await this.getDecryptedCiphers();
|
||||
const decCiphers = await this.getDecryptedCiphers();
|
||||
if (decCiphers != null && decCiphers.length !== 0) {
|
||||
await this.reindexCiphers();
|
||||
return await this.getDecryptedCiphers();
|
||||
@@ -390,10 +412,15 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
return [];
|
||||
}
|
||||
|
||||
decCiphers = await this.decryptCiphers(await this.getAll(), activeUserId);
|
||||
const [newDecCiphers, failedCiphers] = await this.decryptCiphers(
|
||||
await this.getAll(),
|
||||
activeUserId,
|
||||
);
|
||||
|
||||
await this.setDecryptedCipherCache(decCiphers, activeUserId);
|
||||
return decCiphers;
|
||||
await this.setDecryptedCipherCache(newDecCiphers, activeUserId);
|
||||
await this.setFailedDecryptedCiphers(failedCiphers, activeUserId);
|
||||
|
||||
return newDecCiphers;
|
||||
}
|
||||
|
||||
private async getDecryptedCiphers() {
|
||||
@@ -402,7 +429,17 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
);
|
||||
}
|
||||
|
||||
private async decryptCiphers(ciphers: Cipher[], userId: UserId) {
|
||||
/**
|
||||
* Decrypts the provided ciphers using the provided user's keys.
|
||||
* @param ciphers
|
||||
* @param userId
|
||||
* @returns Two cipher arrays, the first containing successfully decrypted ciphers and the second containing ciphers that failed to decrypt.
|
||||
* @private
|
||||
*/
|
||||
private async decryptCiphers(
|
||||
ciphers: Cipher[],
|
||||
userId: UserId,
|
||||
): Promise<[CipherView[], CipherView[]]> {
|
||||
const keys = await firstValueFrom(this.keyService.cipherDecryptionKeys$(userId, true));
|
||||
|
||||
if (keys == null || (keys.userKey == null && Object.keys(keys.orgKeys).length === 0)) {
|
||||
@@ -420,7 +457,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
{} as Record<string, Cipher[]>,
|
||||
);
|
||||
|
||||
const decCiphers = (
|
||||
const allCipherViews = (
|
||||
await Promise.all(
|
||||
Object.entries(grouped).map(async ([orgId, groupedCiphers]) => {
|
||||
if (await this.configService.getFeatureFlag(FeatureFlag.PM4154_BulkEncryptionService)) {
|
||||
@@ -440,7 +477,18 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
.flat()
|
||||
.sort(this.getLocaleSortingFunction());
|
||||
|
||||
return decCiphers;
|
||||
// Split ciphers into two arrays, one for successfully decrypted ciphers and one for ciphers that failed to decrypt
|
||||
return allCipherViews.reduce(
|
||||
(acc, c) => {
|
||||
if (c.decryptionFailure) {
|
||||
acc[1].push(c);
|
||||
} else {
|
||||
acc[0].push(c);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[[], []] as [CipherView[], CipherView[]],
|
||||
);
|
||||
}
|
||||
|
||||
private async reindexCiphers() {
|
||||
@@ -1272,10 +1320,15 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
let encryptedCiphers: CipherWithIdRequest[] = [];
|
||||
|
||||
const ciphers = await firstValueFrom(this.cipherViews$);
|
||||
const failedCiphers = await firstValueFrom(this.failedToDecryptCiphers$);
|
||||
if (!ciphers) {
|
||||
return encryptedCiphers;
|
||||
}
|
||||
|
||||
if (failedCiphers.length > 0) {
|
||||
throw new Error("Cannot rotate ciphers when decryption failures are present");
|
||||
}
|
||||
|
||||
const userCiphers = ciphers.filter((c) => c.organizationId == null);
|
||||
if (userCiphers.length === 0) {
|
||||
return encryptedCiphers;
|
||||
@@ -1636,6 +1689,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
|
||||
private async clearDecryptedCiphersState(userId: UserId) {
|
||||
await this.setDecryptedCiphers(null, userId);
|
||||
await this.setFailedDecryptedCiphers(null, userId);
|
||||
this.clearSortedCiphers();
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,15 @@ export const DECRYPTED_CIPHERS = UserKeyDefinition.record<CipherView>(
|
||||
},
|
||||
);
|
||||
|
||||
export const FAILED_DECRYPTED_CIPHERS = UserKeyDefinition.array<CipherView>(
|
||||
CIPHERS_MEMORY,
|
||||
"failedDecryptedCiphers",
|
||||
{
|
||||
deserializer: (cipher: Jsonify<CipherView>) => CipherView.fromJSON(cipher),
|
||||
clearOn: ["logout", "lock"],
|
||||
},
|
||||
);
|
||||
|
||||
export const LOCAL_DATA_KEY = new UserKeyDefinition<Record<CipherId, LocalData>>(
|
||||
CIPHERS_DISK_LOCAL,
|
||||
"localData",
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<bit-simple-dialog>
|
||||
<i
|
||||
bitDialogIcon
|
||||
class="bwi tw-text-3xl bwi-exclamation-triangle tw-text-warning"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span bitDialogTitle>{{ "decryptionError" | i18n }}</span>
|
||||
<div bitDialogContent>
|
||||
<p>
|
||||
{{ "couldNotDecryptVaultItemsBelow" | i18n }}
|
||||
<a bitLink href="#" (click)="openContactSupport($event)">{{
|
||||
"contactCSToAvoidDataLossPart1" | i18n
|
||||
}}</a>
|
||||
{{ "contactCSToAvoidDataLossPart2" | i18n }}
|
||||
</p>
|
||||
<ul class="tw-list-none tw-pl-0">
|
||||
<li
|
||||
*ngFor="let id of params.cipherIds"
|
||||
class="tw-text-code tw-font-mono tw-py-0.5"
|
||||
(click)="selectText(listItem)"
|
||||
#listItem
|
||||
>
|
||||
{{ id }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="button" bitButton buttonType="primary" (click)="dialogRef.close(false)">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-simple-dialog>
|
||||
@@ -0,0 +1,59 @@
|
||||
import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, inject } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
AnchorLinkDirective,
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
DialogService,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
export type DecryptionFailureDialogParams = {
|
||||
cipherIds: CipherId[];
|
||||
};
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "vault-decryption-failure-dialog",
|
||||
templateUrl: "./decryption-failure-dialog.component.html",
|
||||
imports: [
|
||||
DialogModule,
|
||||
CommonModule,
|
||||
TypographyModule,
|
||||
JslibModule,
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
AnchorLinkDirective,
|
||||
],
|
||||
})
|
||||
export class DecryptionFailureDialogComponent {
|
||||
protected dialogRef = inject(DialogRef);
|
||||
protected params = inject<DecryptionFailureDialogParams>(DIALOG_DATA);
|
||||
protected platformUtilsService = inject(PlatformUtilsService);
|
||||
|
||||
selectText(element: HTMLElement) {
|
||||
const selection = window.getSelection();
|
||||
if (selection == null) {
|
||||
return;
|
||||
}
|
||||
selection.removeAllRanges();
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(element);
|
||||
selection.addRange(range);
|
||||
}
|
||||
|
||||
openContactSupport(event: Event) {
|
||||
event.preventDefault();
|
||||
this.platformUtilsService.launchUri("https://bitwarden.com/contact");
|
||||
}
|
||||
|
||||
static open(dialogService: DialogService, params: DecryptionFailureDialogParams) {
|
||||
return dialogService.open(DecryptionFailureDialogComponent, { data: params });
|
||||
}
|
||||
}
|
||||
@@ -16,5 +16,6 @@ export { DownloadAttachmentComponent } from "./components/download-attachment/do
|
||||
export { PasswordHistoryViewComponent } from "./components/password-history-view/password-history-view.component";
|
||||
export { NewDeviceVerificationNoticePageOneComponent } from "./components/new-device-verification-notice/new-device-verification-notice-page-one.component";
|
||||
export { NewDeviceVerificationNoticePageTwoComponent } from "./components/new-device-verification-notice/new-device-verification-notice-page-two.component";
|
||||
export { DecryptionFailureDialogComponent } from "./components/decryption-failure-dialog/decryption-failure-dialog.component";
|
||||
|
||||
export * as VaultIcons from "./icons";
|
||||
|
||||
Reference in New Issue
Block a user