mirror of
https://github.com/bitwarden/browser
synced 2026-02-14 07:23:45 +00:00
Merge branch 'main' into km/beeep/clean-agent-rewrite
This commit is contained in:
6
.github/workflows/build-browser.yml
vendored
6
.github/workflows/build-browser.yml
vendored
@@ -147,7 +147,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
@@ -248,7 +248,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
@@ -359,7 +359,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
4
.github/workflows/build-cli.yml
vendored
4
.github/workflows/build-cli.yml
vendored
@@ -124,7 +124,7 @@ jobs:
|
||||
awk '{print tolower($0)}')" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
@@ -302,7 +302,7 @@ jobs:
|
||||
choco install nasm --no-progress
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
245
.github/workflows/build-desktop.yml
vendored
245
.github/workflows/build-desktop.yml
vendored
@@ -174,7 +174,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
@@ -323,7 +323,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
@@ -429,7 +429,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
@@ -665,6 +665,239 @@ jobs:
|
||||
path: apps/desktop/dist/nsis-web/${{ needs.setup.outputs.release_channel }}.yml
|
||||
if-no-files-found: error
|
||||
|
||||
windows-beta:
|
||||
name: Windows Beta Build
|
||||
runs-on: windows-2022
|
||||
needs: setup
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
shell: pwsh
|
||||
working-directory: apps/desktop
|
||||
env:
|
||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
|
||||
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
|
||||
NODE_OPTIONS: --max_old_space_size=4096
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
node-version: ${{ env._NODE_VERSION }}
|
||||
|
||||
- name: Install AST
|
||||
run: dotnet tool install --global AzureSignTool --version 4.0.1
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
node --version
|
||||
npm --version
|
||||
choco --version
|
||||
rustup show
|
||||
|
||||
- name: Log in to Azure
|
||||
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
with:
|
||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "code-signing-vault-url,
|
||||
code-signing-client-id,
|
||||
code-signing-tenant-id,
|
||||
code-signing-client-secret,
|
||||
code-signing-cert-name"
|
||||
|
||||
- name: Log out from Azure
|
||||
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Install Node dependencies
|
||||
run: npm ci
|
||||
working-directory: ./
|
||||
|
||||
- name: Download SDK Artifacts
|
||||
if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@main
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
workflow: build-wasm-internal.yml
|
||||
workflow_conclusion: success
|
||||
branch: ${{ inputs.sdk_branch }}
|
||||
artifacts: sdk-internal
|
||||
repo: bitwarden/sdk-internal
|
||||
path: ../sdk-internal
|
||||
if_no_artifact_found: fail
|
||||
|
||||
- name: Override SDK
|
||||
if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }}
|
||||
working-directory: ./
|
||||
run: |
|
||||
ls -l ../
|
||||
npm link ../sdk-internal
|
||||
|
||||
- name: Cache Native Module
|
||||
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
id: cache
|
||||
with:
|
||||
path: |
|
||||
apps/desktop/desktop_native/napi/*.node
|
||||
apps/desktop/desktop_native/dist/*
|
||||
key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }}
|
||||
|
||||
- name: Build Native Module
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
working-directory: apps/desktop/desktop_native
|
||||
run: node build.js cross-platform
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Pack
|
||||
if: ${{ needs.setup.outputs.has_secrets == 'false' }}
|
||||
run: npm run pack:win:beta
|
||||
|
||||
- name: Pack & Sign
|
||||
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
|
||||
env:
|
||||
ELECTRON_BUILDER_SIGN: 1
|
||||
SIGNING_VAULT_URL: ${{ steps.retrieve-secrets.outputs.code-signing-vault-url }}
|
||||
SIGNING_CLIENT_ID: ${{ steps.retrieve-secrets.outputs.code-signing-client-id }}
|
||||
SIGNING_TENANT_ID: ${{ steps.retrieve-secrets.outputs.code-signing-tenant-id }}
|
||||
SIGNING_CLIENT_SECRET: ${{ steps.retrieve-secrets.outputs.code-signing-client-secret }}
|
||||
SIGNING_CERT_NAME: ${{ steps.retrieve-secrets.outputs.code-signing-cert-name }}
|
||||
run: npm run pack:win:beta
|
||||
|
||||
- name: Rename appx files for store
|
||||
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
|
||||
run: |
|
||||
Copy-Item "./dist/Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-ia32.appx" `
|
||||
-Destination "./dist/Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-ia32-store.appx"
|
||||
Copy-Item "./dist/Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-x64.appx" `
|
||||
-Destination "./dist/Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-x64-store.appx"
|
||||
Copy-Item "./dist/Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-arm64.appx" `
|
||||
-Destination "./dist/Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-arm64-store.appx"
|
||||
|
||||
- name: Fix NSIS artifact names for auto-updater
|
||||
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
|
||||
run: |
|
||||
Rename-Item -Path .\dist\nsis-web\Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z `
|
||||
-NewName bitwarden-beta-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z
|
||||
Rename-Item -Path .\dist\nsis-web\Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-x64.nsis.7z `
|
||||
-NewName bitwarden-beta-${{ env._PACKAGE_VERSION }}-x64.nsis.7z
|
||||
Rename-Item -Path .\dist\nsis-web\Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z `
|
||||
-NewName bitwarden-beta-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z
|
||||
|
||||
- name: Upload portable exe artifact
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: Bitwarden-Beta-Portable-${{ env._PACKAGE_VERSION }}.exe
|
||||
path: apps/desktop/dist/Bitwarden-Beta-Portable-${{ env._PACKAGE_VERSION }}.exe
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload installer exe artifact
|
||||
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: Bitwarden-Beta-Installer-${{ env._PACKAGE_VERSION }}.exe
|
||||
path: apps/desktop/dist/nsis-web/Bitwarden-Beta-Installer-${{ env._PACKAGE_VERSION }}.exe
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload appx ia32 artifact
|
||||
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-ia32.appx
|
||||
path: apps/desktop/dist/Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-ia32.appx
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload store appx ia32 artifact
|
||||
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-ia32-store.appx
|
||||
path: apps/desktop/dist/Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-ia32-store.appx
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload NSIS ia32 artifact
|
||||
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: bitwarden-beta-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z
|
||||
path: apps/desktop/dist/nsis-web/bitwarden-beta-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload appx x64 artifact
|
||||
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-x64.appx
|
||||
path: apps/desktop/dist/Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-x64.appx
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload store appx x64 artifact
|
||||
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-x64-store.appx
|
||||
path: apps/desktop/dist/Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-x64-store.appx
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload NSIS x64 artifact
|
||||
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: bitwarden-beta-${{ env._PACKAGE_VERSION }}-x64.nsis.7z
|
||||
path: apps/desktop/dist/nsis-web/bitwarden-beta-${{ env._PACKAGE_VERSION }}-x64.nsis.7z
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload appx ARM64 artifact
|
||||
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-arm64.appx
|
||||
path: apps/desktop/dist/Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-arm64.appx
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload store appx ARM64 artifact
|
||||
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-arm64-store.appx
|
||||
path: apps/desktop/dist/Bitwarden-Beta-${{ env._PACKAGE_VERSION }}-arm64-store.appx
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload NSIS ARM64 artifact
|
||||
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: bitwarden-beta-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z
|
||||
path: apps/desktop/dist/nsis-web/bitwarden-beta-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload auto-update artifact
|
||||
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: ${{ needs.setup.outputs.release_channel }}-beta.yml
|
||||
path: apps/desktop/dist/nsis-web/${{ needs.setup.outputs.release_channel }}.yml
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
macos-build:
|
||||
name: MacOS Build
|
||||
@@ -688,7 +921,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
@@ -914,7 +1147,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
@@ -1172,7 +1405,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
2
.github/workflows/chromatic.yml
vendored
2
.github/workflows/chromatic.yml
vendored
@@ -57,7 +57,7 @@ jobs:
|
||||
echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
node-version: ${{ steps.retrieve-node-version.outputs.node_version }}
|
||||
if: steps.get-changed-files-for-chromatic.outputs.storyFiles == 'true'
|
||||
|
||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -61,7 +61,7 @@ jobs:
|
||||
echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
2
.github/workflows/nx.yml
vendored
2
.github/workflows/nx.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
2
.github/workflows/publish-cli.yml
vendored
2
.github/workflows/publish-cli.yml
vendored
@@ -205,7 +205,7 @@ jobs:
|
||||
echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
node-version: ${{ steps.retrieve-node-version.outputs.node_version }}
|
||||
npm-version: "11.5.1" # FIXME: npm 11.5.1 or later is required to publish w/ OIDC; move version management to somewhere maintainable by automation
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
@@ -25,6 +25,11 @@ export const formatArgsForCodeSnippet = <ComponentType extends Record<string, an
|
||||
const formattedArray = value.map((v) => `'${v}'`).join(", ");
|
||||
return `[${key}]="[${formattedArray}]"`;
|
||||
}
|
||||
|
||||
if (typeof value === "number") {
|
||||
return `[${key}]="${value}"`;
|
||||
}
|
||||
|
||||
return `${key}="${value}"`;
|
||||
})
|
||||
.join(" ");
|
||||
|
||||
10
CLAUDE.md
10
CLAUDE.md
@@ -96,3 +96,13 @@ enum CipherType {
|
||||
```
|
||||
|
||||
Example: `/libs/common/src/vault/enums/cipher-type.ts`
|
||||
|
||||
## References
|
||||
|
||||
- [Web Clients Architecture](https://contributing.bitwarden.com/architecture/clients)
|
||||
- [Architectural Decision Records (ADRs)](https://contributing.bitwarden.com/architecture/adr/)
|
||||
- [Contributing Guide](https://contributing.bitwarden.com/)
|
||||
- [Web Clients Setup Guide](https://contributing.bitwarden.com/getting-started/clients/)
|
||||
- [Code Style](https://contributing.bitwarden.com/contributing/code-style/)
|
||||
- [Security Whitepaper](https://bitwarden.com/help/bitwarden-security-white-paper/)
|
||||
- [Security Definitions](https://contributing.bitwarden.com/architecture/security/definitions)
|
||||
|
||||
494
apps/browser/project.json
Normal file
494
apps/browser/project.json
Normal file
@@ -0,0 +1,494 @@
|
||||
{
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"name": "browser",
|
||||
"projectType": "application",
|
||||
"sourceRoot": "apps/browser/src",
|
||||
"tags": ["scope:browser", "type:app"],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nx/webpack:webpack",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"defaultConfiguration": "chrome-dev",
|
||||
"options": {
|
||||
"outputPath": "dist/apps/browser",
|
||||
"webpackConfig": "apps/browser/webpack.config.js",
|
||||
"tsConfig": "apps/browser/tsconfig.json",
|
||||
"main": "apps/browser/src/popup/main.ts",
|
||||
"target": "web",
|
||||
"compiler": "tsc"
|
||||
},
|
||||
"configurations": {
|
||||
"chrome": {
|
||||
"mode": "production",
|
||||
"outputPath": "dist/apps/browser/chrome",
|
||||
"env": {
|
||||
"BROWSER": "chrome",
|
||||
"MANIFEST_VERSION": "3",
|
||||
"NODE_ENV": "production"
|
||||
}
|
||||
},
|
||||
"chrome-dev": {
|
||||
"mode": "development",
|
||||
"outputPath": "dist/apps/browser/chrome-dev",
|
||||
"env": {
|
||||
"BROWSER": "chrome",
|
||||
"MANIFEST_VERSION": "3",
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
},
|
||||
"edge": {
|
||||
"mode": "production",
|
||||
"outputPath": "dist/apps/browser/edge",
|
||||
"env": {
|
||||
"BROWSER": "edge",
|
||||
"MANIFEST_VERSION": "3",
|
||||
"NODE_ENV": "production"
|
||||
}
|
||||
},
|
||||
"edge-dev": {
|
||||
"mode": "development",
|
||||
"outputPath": "dist/apps/browser/edge-dev",
|
||||
"env": {
|
||||
"BROWSER": "edge",
|
||||
"MANIFEST_VERSION": "3",
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
},
|
||||
"firefox": {
|
||||
"mode": "production",
|
||||
"outputPath": "dist/apps/browser/firefox",
|
||||
"env": {
|
||||
"BROWSER": "firefox",
|
||||
"MANIFEST_VERSION": "3",
|
||||
"NODE_ENV": "production"
|
||||
}
|
||||
},
|
||||
"firefox-dev": {
|
||||
"mode": "development",
|
||||
"outputPath": "dist/apps/browser/firefox-dev",
|
||||
"env": {
|
||||
"BROWSER": "firefox",
|
||||
"MANIFEST_VERSION": "3",
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
},
|
||||
"firefox-mv2": {
|
||||
"mode": "production",
|
||||
"outputPath": "dist/apps/browser/firefox-mv2",
|
||||
"env": {
|
||||
"BROWSER": "firefox",
|
||||
"MANIFEST_VERSION": "2",
|
||||
"NODE_ENV": "production"
|
||||
}
|
||||
},
|
||||
"firefox-mv2-dev": {
|
||||
"mode": "development",
|
||||
"outputPath": "dist/apps/browser/firefox-mv2-dev",
|
||||
"env": {
|
||||
"BROWSER": "firefox",
|
||||
"MANIFEST_VERSION": "2",
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
},
|
||||
"opera": {
|
||||
"mode": "production",
|
||||
"outputPath": "dist/apps/browser/opera",
|
||||
"env": {
|
||||
"BROWSER": "opera",
|
||||
"MANIFEST_VERSION": "3",
|
||||
"NODE_ENV": "production"
|
||||
}
|
||||
},
|
||||
"opera-dev": {
|
||||
"mode": "development",
|
||||
"outputPath": "dist/apps/browser/opera-dev",
|
||||
"env": {
|
||||
"BROWSER": "opera",
|
||||
"MANIFEST_VERSION": "3",
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
},
|
||||
"safari": {
|
||||
"mode": "production",
|
||||
"outputPath": "dist/apps/browser/safari",
|
||||
"env": {
|
||||
"BROWSER": "safari",
|
||||
"MANIFEST_VERSION": "3",
|
||||
"NODE_ENV": "production"
|
||||
}
|
||||
},
|
||||
"safari-dev": {
|
||||
"mode": "development",
|
||||
"outputPath": "dist/apps/browser/safari-dev",
|
||||
"env": {
|
||||
"BROWSER": "safari",
|
||||
"MANIFEST_VERSION": "3",
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
},
|
||||
"safari-mv2": {
|
||||
"mode": "production",
|
||||
"outputPath": "dist/apps/browser/safari-mv2",
|
||||
"env": {
|
||||
"BROWSER": "safari",
|
||||
"MANIFEST_VERSION": "2",
|
||||
"NODE_ENV": "production"
|
||||
}
|
||||
},
|
||||
"safari-mv2-dev": {
|
||||
"mode": "development",
|
||||
"outputPath": "dist/apps/browser/safari-mv2-dev",
|
||||
"env": {
|
||||
"BROWSER": "safari",
|
||||
"MANIFEST_VERSION": "2",
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
},
|
||||
"commercial-chrome": {
|
||||
"mode": "production",
|
||||
"outputPath": "dist/apps/browser/commercial-chrome",
|
||||
"webpackConfig": "bitwarden_license/bit-browser/webpack.config.js",
|
||||
"main": "bitwarden_license/bit-browser/src/popup/main.ts",
|
||||
"tsConfig": "bitwarden_license/bit-browser/tsconfig.json",
|
||||
"env": {
|
||||
"BROWSER": "chrome",
|
||||
"MANIFEST_VERSION": "3",
|
||||
"NODE_ENV": "production"
|
||||
}
|
||||
},
|
||||
"commercial-chrome-dev": {
|
||||
"mode": "development",
|
||||
"outputPath": "dist/apps/browser/commercial-chrome-dev",
|
||||
"webpackConfig": "bitwarden_license/bit-browser/webpack.config.js",
|
||||
"main": "bitwarden_license/bit-browser/src/popup/main.ts",
|
||||
"tsConfig": "bitwarden_license/bit-browser/tsconfig.json",
|
||||
"env": {
|
||||
"BROWSER": "chrome",
|
||||
"MANIFEST_VERSION": "3",
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
},
|
||||
"commercial-edge": {
|
||||
"mode": "production",
|
||||
"outputPath": "dist/apps/browser/commercial-edge",
|
||||
"webpackConfig": "bitwarden_license/bit-browser/webpack.config.js",
|
||||
"main": "bitwarden_license/bit-browser/src/popup/main.ts",
|
||||
"tsConfig": "bitwarden_license/bit-browser/tsconfig.json",
|
||||
"env": {
|
||||
"BROWSER": "edge",
|
||||
"MANIFEST_VERSION": "3",
|
||||
"NODE_ENV": "production"
|
||||
}
|
||||
},
|
||||
"commercial-edge-dev": {
|
||||
"mode": "development",
|
||||
"outputPath": "dist/apps/browser/commercial-edge-dev",
|
||||
"webpackConfig": "bitwarden_license/bit-browser/webpack.config.js",
|
||||
"main": "bitwarden_license/bit-browser/src/popup/main.ts",
|
||||
"tsConfig": "bitwarden_license/bit-browser/tsconfig.json",
|
||||
"env": {
|
||||
"BROWSER": "edge",
|
||||
"MANIFEST_VERSION": "3",
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
},
|
||||
"commercial-firefox": {
|
||||
"mode": "production",
|
||||
"outputPath": "dist/apps/browser/commercial-firefox",
|
||||
"webpackConfig": "bitwarden_license/bit-browser/webpack.config.js",
|
||||
"main": "bitwarden_license/bit-browser/src/popup/main.ts",
|
||||
"tsConfig": "bitwarden_license/bit-browser/tsconfig.json",
|
||||
"env": {
|
||||
"BROWSER": "firefox",
|
||||
"MANIFEST_VERSION": "3",
|
||||
"NODE_ENV": "production"
|
||||
}
|
||||
},
|
||||
"commercial-firefox-dev": {
|
||||
"mode": "development",
|
||||
"outputPath": "dist/apps/browser/commercial-firefox-dev",
|
||||
"webpackConfig": "bitwarden_license/bit-browser/webpack.config.js",
|
||||
"main": "bitwarden_license/bit-browser/src/popup/main.ts",
|
||||
"tsConfig": "bitwarden_license/bit-browser/tsconfig.json",
|
||||
"env": {
|
||||
"BROWSER": "firefox",
|
||||
"MANIFEST_VERSION": "3",
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
},
|
||||
"commercial-firefox-mv2": {
|
||||
"mode": "production",
|
||||
"outputPath": "dist/apps/browser/commercial-firefox-mv2",
|
||||
"webpackConfig": "bitwarden_license/bit-browser/webpack.config.js",
|
||||
"main": "bitwarden_license/bit-browser/src/popup/main.ts",
|
||||
"tsConfig": "bitwarden_license/bit-browser/tsconfig.json",
|
||||
"env": {
|
||||
"BROWSER": "firefox",
|
||||
"MANIFEST_VERSION": "2",
|
||||
"NODE_ENV": "production"
|
||||
}
|
||||
},
|
||||
"commercial-firefox-mv2-dev": {
|
||||
"mode": "development",
|
||||
"outputPath": "dist/apps/browser/commercial-firefox-mv2-dev",
|
||||
"webpackConfig": "bitwarden_license/bit-browser/webpack.config.js",
|
||||
"main": "bitwarden_license/bit-browser/src/popup/main.ts",
|
||||
"tsConfig": "bitwarden_license/bit-browser/tsconfig.json",
|
||||
"env": {
|
||||
"BROWSER": "firefox",
|
||||
"MANIFEST_VERSION": "2",
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
},
|
||||
"commercial-opera": {
|
||||
"mode": "production",
|
||||
"outputPath": "dist/apps/browser/commercial-opera",
|
||||
"webpackConfig": "bitwarden_license/bit-browser/webpack.config.js",
|
||||
"main": "bitwarden_license/bit-browser/src/popup/main.ts",
|
||||
"tsConfig": "bitwarden_license/bit-browser/tsconfig.json",
|
||||
"env": {
|
||||
"BROWSER": "opera",
|
||||
"MANIFEST_VERSION": "3",
|
||||
"NODE_ENV": "production"
|
||||
}
|
||||
},
|
||||
"commercial-opera-dev": {
|
||||
"mode": "development",
|
||||
"outputPath": "dist/apps/browser/commercial-opera-dev",
|
||||
"webpackConfig": "bitwarden_license/bit-browser/webpack.config.js",
|
||||
"main": "bitwarden_license/bit-browser/src/popup/main.ts",
|
||||
"tsConfig": "bitwarden_license/bit-browser/tsconfig.json",
|
||||
"env": {
|
||||
"BROWSER": "opera",
|
||||
"MANIFEST_VERSION": "3",
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
},
|
||||
"commercial-safari": {
|
||||
"mode": "production",
|
||||
"outputPath": "dist/apps/browser/commercial-safari",
|
||||
"webpackConfig": "bitwarden_license/bit-browser/webpack.config.js",
|
||||
"main": "bitwarden_license/bit-browser/src/popup/main.ts",
|
||||
"tsConfig": "bitwarden_license/bit-browser/tsconfig.json",
|
||||
"env": {
|
||||
"BROWSER": "safari",
|
||||
"MANIFEST_VERSION": "3",
|
||||
"NODE_ENV": "production"
|
||||
}
|
||||
},
|
||||
"commercial-safari-dev": {
|
||||
"mode": "development",
|
||||
"outputPath": "dist/apps/browser/commercial-safari-dev",
|
||||
"webpackConfig": "bitwarden_license/bit-browser/webpack.config.js",
|
||||
"main": "bitwarden_license/bit-browser/src/popup/main.ts",
|
||||
"tsConfig": "bitwarden_license/bit-browser/tsconfig.json",
|
||||
"env": {
|
||||
"BROWSER": "safari",
|
||||
"MANIFEST_VERSION": "3",
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
},
|
||||
"commercial-safari-mv2": {
|
||||
"mode": "production",
|
||||
"outputPath": "dist/apps/browser/commercial-safari-mv2",
|
||||
"webpackConfig": "bitwarden_license/bit-browser/webpack.config.js",
|
||||
"main": "bitwarden_license/bit-browser/src/popup/main.ts",
|
||||
"tsConfig": "bitwarden_license/bit-browser/tsconfig.json",
|
||||
"env": {
|
||||
"BROWSER": "safari",
|
||||
"MANIFEST_VERSION": "2",
|
||||
"NODE_ENV": "production"
|
||||
}
|
||||
},
|
||||
"commercial-safari-mv2-dev": {
|
||||
"mode": "development",
|
||||
"outputPath": "dist/apps/browser/commercial-safari-mv2-dev",
|
||||
"webpackConfig": "bitwarden_license/bit-browser/webpack.config.js",
|
||||
"main": "bitwarden_license/bit-browser/src/popup/main.ts",
|
||||
"tsConfig": "bitwarden_license/bit-browser/tsconfig.json",
|
||||
"env": {
|
||||
"BROWSER": "safari",
|
||||
"MANIFEST_VERSION": "2",
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"executor": "@nx/webpack:webpack",
|
||||
"defaultConfiguration": "chrome-dev",
|
||||
"options": {
|
||||
"outputPath": "dist/apps/browser",
|
||||
"webpackConfig": "apps/browser/webpack.config.js",
|
||||
"tsConfig": "apps/browser/tsconfig.json",
|
||||
"main": "apps/browser/src/popup/main.ts",
|
||||
"target": "web",
|
||||
"compiler": "tsc",
|
||||
"watch": true
|
||||
},
|
||||
"configurations": {
|
||||
"chrome-dev": {
|
||||
"mode": "development",
|
||||
"outputPath": "dist/apps/browser/chrome-dev",
|
||||
"env": {
|
||||
"BROWSER": "chrome",
|
||||
"MANIFEST_VERSION": "3",
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
},
|
||||
"firefox-dev": {
|
||||
"mode": "development",
|
||||
"outputPath": "dist/apps/browser/firefox-dev",
|
||||
"env": {
|
||||
"BROWSER": "firefox",
|
||||
"MANIFEST_VERSION": "3",
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
},
|
||||
"firefox-mv2-dev": {
|
||||
"mode": "development",
|
||||
"outputPath": "dist/apps/browser/firefox-mv2-dev",
|
||||
"env": {
|
||||
"BROWSER": "firefox",
|
||||
"MANIFEST_VERSION": "2",
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
},
|
||||
"safari-dev": {
|
||||
"mode": "development",
|
||||
"outputPath": "dist/apps/browser/safari-dev",
|
||||
"env": {
|
||||
"BROWSER": "safari",
|
||||
"MANIFEST_VERSION": "3",
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
},
|
||||
"safari-mv2-dev": {
|
||||
"mode": "development",
|
||||
"outputPath": "dist/apps/browser/safari-mv2-dev",
|
||||
"env": {
|
||||
"BROWSER": "safari",
|
||||
"MANIFEST_VERSION": "2",
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
},
|
||||
"edge-dev": {
|
||||
"mode": "development",
|
||||
"outputPath": "dist/apps/browser/edge-dev",
|
||||
"env": {
|
||||
"BROWSER": "edge",
|
||||
"MANIFEST_VERSION": "3",
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
},
|
||||
"opera-dev": {
|
||||
"mode": "development",
|
||||
"outputPath": "dist/apps/browser/opera-dev",
|
||||
"env": {
|
||||
"BROWSER": "opera",
|
||||
"MANIFEST_VERSION": "3",
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
},
|
||||
"commercial-chrome-dev": {
|
||||
"mode": "development",
|
||||
"outputPath": "dist/apps/browser/commercial-chrome-dev",
|
||||
"webpackConfig": "bitwarden_license/bit-browser/webpack.config.js",
|
||||
"main": "bitwarden_license/bit-browser/src/popup/main.ts",
|
||||
"tsConfig": "bitwarden_license/bit-browser/tsconfig.json",
|
||||
"env": {
|
||||
"BROWSER": "chrome",
|
||||
"MANIFEST_VERSION": "3",
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
},
|
||||
"commercial-firefox-dev": {
|
||||
"mode": "development",
|
||||
"outputPath": "dist/apps/browser/commercial-firefox-dev",
|
||||
"webpackConfig": "bitwarden_license/bit-browser/webpack.config.js",
|
||||
"main": "bitwarden_license/bit-browser/src/popup/main.ts",
|
||||
"tsConfig": "bitwarden_license/bit-browser/tsconfig.json",
|
||||
"env": {
|
||||
"BROWSER": "firefox",
|
||||
"MANIFEST_VERSION": "3",
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
},
|
||||
"commercial-firefox-mv2-dev": {
|
||||
"mode": "development",
|
||||
"outputPath": "dist/apps/browser/commercial-firefox-mv2-dev",
|
||||
"webpackConfig": "bitwarden_license/bit-browser/webpack.config.js",
|
||||
"main": "bitwarden_license/bit-browser/src/popup/main.ts",
|
||||
"tsConfig": "bitwarden_license/bit-browser/tsconfig.json",
|
||||
"env": {
|
||||
"BROWSER": "firefox",
|
||||
"MANIFEST_VERSION": "2",
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
},
|
||||
"commercial-safari-dev": {
|
||||
"mode": "development",
|
||||
"outputPath": "dist/apps/browser/commercial-safari-dev",
|
||||
"webpackConfig": "bitwarden_license/bit-browser/webpack.config.js",
|
||||
"main": "bitwarden_license/bit-browser/src/popup/main.ts",
|
||||
"tsConfig": "bitwarden_license/bit-browser/tsconfig.json",
|
||||
"env": {
|
||||
"BROWSER": "safari",
|
||||
"MANIFEST_VERSION": "3",
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
},
|
||||
"commercial-safari-mv2-dev": {
|
||||
"mode": "development",
|
||||
"outputPath": "dist/apps/browser/commercial-safari-mv2-dev",
|
||||
"webpackConfig": "bitwarden_license/bit-browser/webpack.config.js",
|
||||
"main": "bitwarden_license/bit-browser/src/popup/main.ts",
|
||||
"tsConfig": "bitwarden_license/bit-browser/tsconfig.json",
|
||||
"env": {
|
||||
"BROWSER": "safari",
|
||||
"MANIFEST_VERSION": "2",
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
},
|
||||
"commercial-edge-dev": {
|
||||
"mode": "development",
|
||||
"outputPath": "dist/apps/browser/commercial-edge-dev",
|
||||
"webpackConfig": "bitwarden_license/bit-browser/webpack.config.js",
|
||||
"main": "bitwarden_license/bit-browser/src/popup/main.ts",
|
||||
"tsConfig": "bitwarden_license/bit-browser/tsconfig.json",
|
||||
"env": {
|
||||
"BROWSER": "edge",
|
||||
"MANIFEST_VERSION": "3",
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
},
|
||||
"commercial-opera-dev": {
|
||||
"mode": "development",
|
||||
"outputPath": "dist/apps/browser/commercial-opera-dev",
|
||||
"webpackConfig": "bitwarden_license/bit-browser/webpack.config.js",
|
||||
"main": "bitwarden_license/bit-browser/src/popup/main.ts",
|
||||
"tsConfig": "bitwarden_license/bit-browser/tsconfig.json",
|
||||
"env": {
|
||||
"BROWSER": "opera",
|
||||
"MANIFEST_VERSION": "3",
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "apps/browser/jest.config.js"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint",
|
||||
"outputs": ["{options.outputFile}"],
|
||||
"options": {
|
||||
"lintFilePatterns": ["apps/browser/**/*.ts", "apps/browser/**/*.html"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -558,7 +558,7 @@
|
||||
"message": "Archive",
|
||||
"description": "Verb"
|
||||
},
|
||||
"unarchive": {
|
||||
"unArchive": {
|
||||
"message": "Unarchive"
|
||||
},
|
||||
"itemsInArchive": {
|
||||
@@ -570,11 +570,11 @@
|
||||
"noItemsInArchiveDesc": {
|
||||
"message": "Archived items will appear here and will be excluded from general search results and autofill suggestions."
|
||||
},
|
||||
"itemSentToArchive": {
|
||||
"message": "Item sent to archive"
|
||||
"itemWasSentToArchive": {
|
||||
"message": "Item was sent to archive"
|
||||
},
|
||||
"itemRemovedFromArchive": {
|
||||
"message": "Item removed from archive"
|
||||
"itemUnarchived": {
|
||||
"message": "Item was unarchived"
|
||||
},
|
||||
"archiveItem": {
|
||||
"message": "Archive item"
|
||||
@@ -5579,17 +5579,37 @@
|
||||
"hasItemsVaultNudgeTitle": {
|
||||
"message": "Welcome to your vault!"
|
||||
},
|
||||
"phishingPageTitle":{
|
||||
"message": "Phishing website"
|
||||
"phishingPageTitleV2":{
|
||||
"message": "Phishing attempt detected"
|
||||
},
|
||||
"phishingPageCloseTab": {
|
||||
"message": "Close tab"
|
||||
"phishingPageSummary": {
|
||||
"message": "The site you are attempting to visit is a known malicious site and a security risk."
|
||||
},
|
||||
"phishingPageContinue": {
|
||||
"message": "Continue"
|
||||
"phishingPageCloseTabV2": {
|
||||
"message": "Close this tab"
|
||||
},
|
||||
"phishingPageLearnWhy": {
|
||||
"message": "Why are you seeing this?"
|
||||
"phishingPageContinueV2": {
|
||||
"message": "Continue to this site (not recommended)"
|
||||
},
|
||||
"phishingPageExplanation1": {
|
||||
"message": "This site was found in ",
|
||||
"description": "This is in multiple parts to allow for bold text in the middle of the sentence. A proper name follows this."
|
||||
},
|
||||
"phishingPageExplanation2": {
|
||||
"message": ", an open-source list of known phishing sites used for stealing personal and sensitive information.",
|
||||
"description": "This is in multiple parts to allow for bold text in the middle of the sentence. A proper name precedes this."
|
||||
},
|
||||
"phishingPageLearnMore" : {
|
||||
"message": "Learn more about phishing detection"
|
||||
},
|
||||
"protectedBy": {
|
||||
"message": "Protected by $PRODUCT$",
|
||||
"placeholders": {
|
||||
"product": {
|
||||
"content": "$1",
|
||||
"example": "Bitwarden Phishing Blocker"
|
||||
}
|
||||
}
|
||||
},
|
||||
"hasItemsVaultNudgeBodyOne": {
|
||||
"message": "Autofill items for the current page"
|
||||
|
||||
@@ -172,7 +172,9 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
const showOnLocked =
|
||||
!this.platformUtilsService.isFirefox() && !this.platformUtilsService.isSafari();
|
||||
!this.platformUtilsService.isFirefox() &&
|
||||
!this.platformUtilsService.isSafari() &&
|
||||
!(this.platformUtilsService.isOpera() && navigator.platform === "MacIntel");
|
||||
|
||||
this.vaultTimeoutOptions = [
|
||||
{ name: this.i18nService.t("immediately"), value: 0 },
|
||||
|
||||
@@ -9,15 +9,17 @@
|
||||
<app-vault-icon slot="start" [cipher]="cipher"></app-vault-icon>
|
||||
<span data-testid="item-name">
|
||||
{{ cipher.name }}
|
||||
<i
|
||||
*ngIf="cipher.organizationId"
|
||||
[appA11yTitle]="'shared' | i18n"
|
||||
class="bwi bwi-collection-shared text-muted"
|
||||
></i>
|
||||
@if (cipher.organizationId) {
|
||||
<i [appA11yTitle]="'shared' | i18n" class="bwi bwi-collection-shared tw-text-muted"></i>
|
||||
}
|
||||
</span>
|
||||
<ng-container slot="secondary">
|
||||
<div *ngIf="getSubName(cipher)">{{ getSubName(cipher) }}</div>
|
||||
<div *ngIf="cipher.subTitle">{{ cipher.subTitle }}</div>
|
||||
@if (getSubName(cipher)) {
|
||||
<div>{{ getSubName(cipher) }}</div>
|
||||
}
|
||||
@if (cipher.subTitle) {
|
||||
<div>{{ cipher.subTitle }}</div>
|
||||
}
|
||||
</ng-container>
|
||||
</button>
|
||||
</bit-item>
|
||||
|
||||
@@ -1,52 +1,24 @@
|
||||
<ng-container *ngIf="(fido2PopoutSessionData$ | async).fallbackSupported">
|
||||
<div class="useBrowserlink">
|
||||
<button
|
||||
type="button"
|
||||
(click)="toggle()"
|
||||
cdkOverlayOrigin
|
||||
#trigger="cdkOverlayOrigin"
|
||||
aria-haspopup="dialog"
|
||||
aria-controls="cdk-overlay-container"
|
||||
>
|
||||
<span class="text-primary">
|
||||
@if ((fido2PopoutSessionData$ | async).fallbackSupported) {
|
||||
<div class="tw-flex tw-items-center tw-justify-center tw-p-2">
|
||||
<button type="button" [bitMenuTriggerFor]="deviceMenu">
|
||||
<span bitTypography="body2">
|
||||
{{ "useDeviceOrHardwareKey" | i18n }}
|
||||
</span>
|
||||
<i class="bwi bwi-fw bwi-sm bwi-angle-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
<bit-menu #deviceMenu>
|
||||
<button type="button" bitMenuItem (click)="abort(false)">
|
||||
{{ "justOnce" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem (click)="abort()">
|
||||
{{ "alwaysForThisSite" | i18n }}
|
||||
</button>
|
||||
</bit-menu>
|
||||
</div>
|
||||
|
||||
<ng-template
|
||||
cdkConnectedOverlay
|
||||
[cdkConnectedOverlayOrigin]="trigger"
|
||||
[cdkConnectedOverlayOpen]="isOpen"
|
||||
[cdkConnectedOverlayPositions]="overlayPosition"
|
||||
[cdkConnectedOverlayHasBackdrop]="true"
|
||||
[cdkConnectedOverlayBackdropClass]="'cdk-overlay-transparent-backdrop'"
|
||||
(backdropClick)="isOpen = false"
|
||||
(detach)="close()"
|
||||
>
|
||||
<div class="box-content">
|
||||
<div
|
||||
class="fido2-browser-selector-dropdown"
|
||||
[@transformPanel]="'open'"
|
||||
cdkTrapFocus
|
||||
cdkTrapFocusAutoCapture
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<button type="button" class="fido2-browser-selector-dropdown-item" (click)="abort(false)">
|
||||
<span>{{ "justOnce" | i18n }}</span>
|
||||
</button>
|
||||
<br />
|
||||
<button type="button" class="fido2-browser-selector-dropdown-item" (click)="abort()">
|
||||
<span>{{ "alwaysForThisSite" | i18n }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<div
|
||||
*ngIf="showOverlay"
|
||||
class="tw-absolute tw-size-full tw-bg-background-alt tw-inset-0 tw-bg-opacity-80 tw-z-50"
|
||||
></div>
|
||||
</ng-container>
|
||||
@if (showOverlay) {
|
||||
<div
|
||||
class="tw-absolute tw-size-full tw-bg-background-alt tw-inset-0 tw-bg-opacity-80 tw-z-50"
|
||||
></div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { animate, state, style, transition, trigger } from "@angular/animations";
|
||||
import { A11yModule } from "@angular/cdk/a11y";
|
||||
import { ConnectedPosition, CdkOverlayOrigin, CdkConnectedOverlay } from "@angular/cdk/overlay";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
@@ -13,6 +10,7 @@ import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { MenuModule } from "@bitwarden/components";
|
||||
|
||||
import { fido2PopoutSessionData$ } from "../../../vault/popup/utils/fido2-popout-session-data";
|
||||
import { BrowserFido2UserInterfaceSession } from "../../fido2/services/browser-fido2-user-interface.service";
|
||||
@@ -20,63 +18,24 @@ import { BrowserFido2UserInterfaceSession } from "../../fido2/services/browser-f
|
||||
@Component({
|
||||
selector: "app-fido2-use-browser-link",
|
||||
templateUrl: "fido2-use-browser-link.component.html",
|
||||
imports: [A11yModule, CdkConnectedOverlay, CdkOverlayOrigin, CommonModule, JslibModule],
|
||||
animations: [
|
||||
trigger("transformPanel", [
|
||||
state(
|
||||
"void",
|
||||
style({
|
||||
opacity: 0,
|
||||
}),
|
||||
),
|
||||
transition(
|
||||
"void => open",
|
||||
animate(
|
||||
"100ms linear",
|
||||
style({
|
||||
opacity: 1,
|
||||
}),
|
||||
),
|
||||
),
|
||||
transition("* => void", animate("100ms linear", style({ opacity: 0 }))),
|
||||
]),
|
||||
],
|
||||
imports: [CommonModule, JslibModule, MenuModule],
|
||||
})
|
||||
export class Fido2UseBrowserLinkComponent {
|
||||
showOverlay = false;
|
||||
isOpen = false;
|
||||
overlayPosition: ConnectedPosition[] = [
|
||||
{
|
||||
originX: "start",
|
||||
originY: "bottom",
|
||||
overlayX: "start",
|
||||
overlayY: "top",
|
||||
offsetY: 5,
|
||||
},
|
||||
];
|
||||
|
||||
protected fido2PopoutSessionData$ = fido2PopoutSessionData$();
|
||||
|
||||
constructor(
|
||||
private domainSettingsService: DomainSettingsService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private readonly domainSettingsService: DomainSettingsService,
|
||||
private readonly platformUtilsService: PlatformUtilsService,
|
||||
private readonly i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
toggle() {
|
||||
this.isOpen = !this.isOpen;
|
||||
}
|
||||
|
||||
close() {
|
||||
this.isOpen = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aborts the current FIDO2 session and fallsback to the browser.
|
||||
* @param excludeDomain - Identifies if the domain should be excluded from future FIDO2 prompts.
|
||||
*/
|
||||
protected async abort(excludeDomain = true) {
|
||||
this.close();
|
||||
const sessionData = await firstValueFrom(this.fido2PopoutSessionData$);
|
||||
|
||||
if (!excludeDomain) {
|
||||
|
||||
@@ -1,144 +1,140 @@
|
||||
<popup-page *ngIf="data$ | async as data">
|
||||
<popup-header
|
||||
slot="header"
|
||||
pageTitle="{{
|
||||
(passkeyAction === PasskeyActions.Register ? 'savePasskey' : 'logInWithPasskeyQuestion')
|
||||
| i18n
|
||||
}}"
|
||||
>
|
||||
<button
|
||||
*ngIf="showNewPasskeyButton"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
type="button"
|
||||
(click)="addCipher()"
|
||||
slot="end"
|
||||
@if (data$ | async; as data) {
|
||||
<popup-page>
|
||||
<popup-header
|
||||
slot="header"
|
||||
pageTitle="{{
|
||||
(passkeyAction === PasskeyActions.Register ? 'savePasskey' : 'logInWithPasskeyQuestion')
|
||||
| i18n
|
||||
}}"
|
||||
>
|
||||
<i class="bwi bwi-plus" aria-hidden="true"></i>
|
||||
{{ "new" | i18n }}
|
||||
</button>
|
||||
</popup-header>
|
||||
@if (showNewPasskeyButton) {
|
||||
<button bitButton buttonType="primary" type="button" (click)="addCipher()" slot="end">
|
||||
<i class="bwi bwi-plus" aria-hidden="true"></i>
|
||||
{{ "new" | i18n }}
|
||||
</button>
|
||||
}
|
||||
</popup-header>
|
||||
|
||||
<div class="tw-p-2">
|
||||
<bit-section *ngIf="passkeyAction === PasskeyActions.Register">
|
||||
<bit-search
|
||||
appAutofocus
|
||||
autocomplete="off"
|
||||
id="search"
|
||||
placeholder="{{ 'searchVault' | i18n }}"
|
||||
(ngModelChange)="search()"
|
||||
[(ngModel)]="searchText"
|
||||
></bit-search>
|
||||
</bit-section>
|
||||
<div class="tw-p-2">
|
||||
@if (passkeyAction === PasskeyActions.Register) {
|
||||
<bit-section>
|
||||
<bit-search
|
||||
appAutofocus
|
||||
autocomplete="off"
|
||||
id="search"
|
||||
placeholder="{{ 'searchVault' | i18n }}"
|
||||
(ngModelChange)="search()"
|
||||
[(ngModel)]="searchText"
|
||||
></bit-search>
|
||||
</bit-section>
|
||||
}
|
||||
|
||||
<!-- Display when adding a new passkey -->
|
||||
<bit-section *ngIf="data.message.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest">
|
||||
<!-- Display when matching ciphers (i.e. same domain, no passkeys) exist -->
|
||||
<ng-container *ngIf="displayedCiphers.length > 0">
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">{{ "chooseCipherForPasskeySave" | i18n }}</h2>
|
||||
</bit-section-header>
|
||||
<app-fido2-cipher-row
|
||||
*ngFor="let cipherItem of displayedCiphers"
|
||||
[cipher]="cipherItem"
|
||||
title="{{ 'passkeyItem' | i18n }}"
|
||||
(onSelected)="handleCipherItemSelect($event)"
|
||||
></app-fido2-cipher-row>
|
||||
</ng-container>
|
||||
@switch (data.message.type) {
|
||||
@case (BrowserFido2MessageTypes.ConfirmNewCredentialRequest) {
|
||||
<bit-section>
|
||||
@if (displayedCiphers.length > 0) {
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">{{ "chooseCipherForPasskeySave" | i18n }}</h2>
|
||||
</bit-section-header>
|
||||
@for (cipherItem of displayedCiphers; track cipherItem.id) {
|
||||
<app-fido2-cipher-row
|
||||
[cipher]="cipherItem"
|
||||
title="{{ 'passkeyItem' | i18n }}"
|
||||
(onSelected)="handleCipherItemSelect($event)"
|
||||
></app-fido2-cipher-row>
|
||||
}
|
||||
}
|
||||
@if (!displayedCiphers.length) {
|
||||
<bit-no-items [icon]="noResultsIcon">
|
||||
<ng-container slot="title">{{
|
||||
(hasSearched ? "noItemsMatchSearch" : "noMatchingLoginsForSite") | i18n
|
||||
}}</ng-container>
|
||||
<ng-container slot="description">{{
|
||||
(hasSearched ? "searchSavePasskeyNewLogin" : "clearFiltersOrTryAnother") | i18n
|
||||
}}</ng-container>
|
||||
|
||||
<!-- Display when no matching ciphers exist -->
|
||||
<ng-container *ngIf="!displayedCiphers.length">
|
||||
<bit-no-items class="tw-text-main" [icon]="noResultsIcon">
|
||||
<ng-container slot="title">{{
|
||||
(hasSearched ? "noItemsMatchSearch" : "noMatchingLoginsForSite") | i18n
|
||||
}}</ng-container>
|
||||
<ng-container slot="description">{{
|
||||
(hasSearched ? "searchSavePasskeyNewLogin" : "clearFiltersOrTryAnother") | i18n
|
||||
}}</ng-container>
|
||||
|
||||
<button
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
slot="button"
|
||||
type="button"
|
||||
(click)="hasSearched ? clearSearch() : saveNewLogin()"
|
||||
[loading]="loading"
|
||||
>
|
||||
{{ (hasSearched ? "multiSelectClearAll" : "savePasskeyNewLogin") | i18n }}
|
||||
</button>
|
||||
</bit-no-items>
|
||||
</ng-container>
|
||||
</bit-section>
|
||||
|
||||
<!-- Display when the passkey being saved already exists -->
|
||||
<bit-section
|
||||
*ngIf="data.message.type === BrowserFido2MessageTypes.InformExcludedCredentialRequest"
|
||||
>
|
||||
<div class="auth-flow">
|
||||
<p class="subtitle">{{ "passkeyAlreadyExists" | i18n }}</p>
|
||||
<div class="box list">
|
||||
<div class="box-content">
|
||||
<app-fido2-cipher-row
|
||||
*ngFor="let cipherItem of displayedCiphers"
|
||||
[cipher]="cipherItem"
|
||||
title="{{ 'passkeyItem' | i18n }}"
|
||||
(onSelected)="handleCipherItemSelect($event)"
|
||||
></app-fido2-cipher-row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</bit-section>
|
||||
|
||||
<!-- Display when picking a passkey to login with -->
|
||||
<bit-section *ngIf="data.message.type === BrowserFido2MessageTypes.PickCredentialRequest">
|
||||
<!-- Display when matching ciphers exist -->
|
||||
<ng-container *ngIf="displayedCiphers.length > 0">
|
||||
<ng-container slot="title">{{ "chooseCipherForPasskeyAuth" | i18n }}</ng-container>
|
||||
<app-fido2-cipher-row
|
||||
*ngFor="let cipherItem of displayedCiphers"
|
||||
[cipher]="cipherItem"
|
||||
title="{{ 'passkeyItem' | i18n }}"
|
||||
(onSelected)="handleCipherItemSelect($event)"
|
||||
></app-fido2-cipher-row>
|
||||
</ng-container>
|
||||
|
||||
<!-- Display when no matching ciphers exist -->
|
||||
<ng-container *ngIf="!displayedCiphers.length">
|
||||
<bit-no-items class="tw-text-main" [icon]="noResultsIcon">
|
||||
<ng-container slot="title">{{
|
||||
(hasSearched ? "noItemsMatchSearch" : "noMatchingLoginsForSite") | i18n
|
||||
}}</ng-container>
|
||||
<ng-container slot="description">{{
|
||||
(hasSearched ? "searchSavePasskeyNewLogin" : "clearFiltersOrTryAnother") | i18n
|
||||
}}</ng-container>
|
||||
|
||||
<button
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
slot="button"
|
||||
type="button"
|
||||
(click)="hasSearched ? clearSearch() : saveNewLogin()"
|
||||
[loading]="loading"
|
||||
>
|
||||
{{ (hasSearched ? "multiSelectClearAll" : "savePasskeyNewLogin") | i18n }}
|
||||
</button>
|
||||
</bit-no-items>
|
||||
</ng-container>
|
||||
</bit-section>
|
||||
|
||||
<!-- Display when initiating passkey login, but no cooresponding cipher is found in the vault -->
|
||||
<bit-section
|
||||
*ngIf="data.message.type === BrowserFido2MessageTypes.InformCredentialNotFoundRequest"
|
||||
>
|
||||
<div class="auth-flow">
|
||||
<p class="subtitle">{{ "noPasskeysFoundForThisApplication" | i18n }}</p>
|
||||
</div>
|
||||
<button type="button" class="btn primary block" (click)="abort(false)">
|
||||
<span [hidden]="loading">{{ "close" | i18n }}</span>
|
||||
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!loading" aria-hidden="true"></i>
|
||||
</button>
|
||||
</bit-section>
|
||||
|
||||
<app-fido2-use-browser-link></app-fido2-use-browser-link>
|
||||
</div>
|
||||
</popup-page>
|
||||
<button
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
slot="button"
|
||||
type="button"
|
||||
(click)="hasSearched ? clearSearch() : saveNewLogin()"
|
||||
[loading]="loading"
|
||||
>
|
||||
{{ (hasSearched ? "multiSelectClearAll" : "savePasskeyNewLogin") | i18n }}
|
||||
</button>
|
||||
</bit-no-items>
|
||||
}
|
||||
</bit-section>
|
||||
}
|
||||
@case (BrowserFido2MessageTypes.InformExcludedCredentialRequest) {
|
||||
<bit-section>
|
||||
<div class="tw-space-y-4">
|
||||
<p>{{ "passkeyAlreadyExists" | i18n }}</p>
|
||||
<div class="tw-divide-y tw-divide-secondary-300">
|
||||
@for (cipherItem of displayedCiphers; track cipherItem.id) {
|
||||
<app-fido2-cipher-row
|
||||
[cipher]="cipherItem"
|
||||
title="{{ 'passkeyItem' | i18n }}"
|
||||
(onSelected)="handleCipherItemSelect($event)"
|
||||
></app-fido2-cipher-row>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</bit-section>
|
||||
}
|
||||
@case (BrowserFido2MessageTypes.PickCredentialRequest) {
|
||||
<bit-section>
|
||||
@if (displayedCiphers.length > 0) {
|
||||
<ng-container slot="title">{{ "chooseCipherForPasskeyAuth" | i18n }}</ng-container>
|
||||
@for (cipherItem of displayedCiphers; track cipherItem.id) {
|
||||
<app-fido2-cipher-row
|
||||
[cipher]="cipherItem"
|
||||
title="{{ 'passkeyItem' | i18n }}"
|
||||
(onSelected)="handleCipherItemSelect($event)"
|
||||
></app-fido2-cipher-row>
|
||||
}
|
||||
} @else {
|
||||
<bit-no-items [icon]="noResultsIcon">
|
||||
<ng-container slot="title">{{
|
||||
(hasSearched ? "noItemsMatchSearch" : "noMatchingLoginsForSite") | i18n
|
||||
}}</ng-container>
|
||||
<ng-container slot="description">{{
|
||||
(hasSearched ? "searchSavePasskeyNewLogin" : "clearFiltersOrTryAnother") | i18n
|
||||
}}</ng-container>
|
||||
<button
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
slot="button"
|
||||
type="button"
|
||||
(click)="hasSearched ? clearSearch() : saveNewLogin()"
|
||||
[loading]="loading"
|
||||
>
|
||||
{{ (hasSearched ? "multiSelectClearAll" : "savePasskeyNewLogin") | i18n }}
|
||||
</button>
|
||||
</bit-no-items>
|
||||
}
|
||||
</bit-section>
|
||||
}
|
||||
@case (BrowserFido2MessageTypes.InformCredentialNotFoundRequest) {
|
||||
<bit-section>
|
||||
<div class="tw-space-y-4">
|
||||
<p>{{ "noPasskeysFoundForThisApplication" | i18n }}</p>
|
||||
</div>
|
||||
<button
|
||||
bitButton
|
||||
block
|
||||
buttonType="primary"
|
||||
type="button"
|
||||
(click)="abort(false)"
|
||||
[loading]="loading"
|
||||
>
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</bit-section>
|
||||
}
|
||||
}
|
||||
<app-fido2-use-browser-link></app-fido2-use-browser-link>
|
||||
</div>
|
||||
</popup-page>
|
||||
}
|
||||
|
||||
@@ -4105,6 +4105,7 @@ describe("AutofillService", () => {
|
||||
});
|
||||
|
||||
it("returns null if the field cannot be hidden", () => {
|
||||
usernameField.form = "differentFormId";
|
||||
const result = autofillService["findUsernameField"](
|
||||
pageDetails,
|
||||
passwordField,
|
||||
@@ -4116,6 +4117,18 @@ describe("AutofillService", () => {
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it("returns the field if the username field is in the form", () => {
|
||||
const result = autofillService["findUsernameField"](
|
||||
pageDetails,
|
||||
passwordField,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
|
||||
expect(result).toBe(usernameField);
|
||||
});
|
||||
|
||||
it("returns the field if the field can be hidden", () => {
|
||||
const result = autofillService["findUsernameField"](
|
||||
pageDetails,
|
||||
|
||||
@@ -2286,11 +2286,16 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
this.findMatchingFieldIndex(f, AutoFillConstants.UsernameFieldNames) > -1;
|
||||
const isInSameForm = f.form === passwordField.form;
|
||||
|
||||
// An email or tel field in the same form as the password field is likely a qualified
|
||||
// candidate for autofill, even if visibility checks are unreliable
|
||||
const isQualifiedUsernameField =
|
||||
f.form === passwordField.form && (f.type === "email" || f.type === "tel");
|
||||
|
||||
if (
|
||||
!f.disabled &&
|
||||
(canBeReadOnly || !f.readonly) &&
|
||||
(withoutForm || isInSameForm || includesUsernameFieldName) &&
|
||||
(canBeHidden || f.viewable) &&
|
||||
(canBeHidden || f.viewable || isQualifiedUsernameField) &&
|
||||
(f.type === "text" || f.type === "email" || f.type === "tel")
|
||||
) {
|
||||
// Prioritize fields in the same form as the password field
|
||||
|
||||
@@ -100,6 +100,8 @@ import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-
|
||||
import { MasterPasswordService } from "@bitwarden/common/key-management/master-password/services/master-password.service";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { PinService } from "@bitwarden/common/key-management/pin/pin.service.implementation";
|
||||
import { SecurityStateService } from "@bitwarden/common/key-management/security-state/abstractions/security-state.service";
|
||||
import { DefaultSecurityStateService } from "@bitwarden/common/key-management/security-state/services/security-state.service";
|
||||
import { DefaultProcessReloadService } from "@bitwarden/common/key-management/services/default-process-reload.service";
|
||||
import {
|
||||
DefaultVaultTimeoutSettingsService,
|
||||
@@ -452,6 +454,7 @@ export default class MainBackground {
|
||||
taskService: TaskService;
|
||||
cipherEncryptionService: CipherEncryptionService;
|
||||
private restrictedItemTypesService: RestrictedItemTypesService;
|
||||
private securityStateService: SecurityStateService;
|
||||
|
||||
ipcContentScriptManagerService: IpcContentScriptManagerService;
|
||||
ipcService: IpcService;
|
||||
@@ -668,6 +671,8 @@ export default class MainBackground {
|
||||
logoutCallback,
|
||||
);
|
||||
|
||||
this.securityStateService = new DefaultSecurityStateService(this.stateProvider);
|
||||
|
||||
this.popupViewCacheBackgroundService = new PopupViewCacheBackgroundService(
|
||||
messageListener,
|
||||
this.globalStateProvider,
|
||||
@@ -830,6 +835,7 @@ export default class MainBackground {
|
||||
this.accountService,
|
||||
this.kdfConfigService,
|
||||
this.keyService,
|
||||
this.securityStateService,
|
||||
this.apiService,
|
||||
this.stateProvider,
|
||||
this.configService,
|
||||
@@ -984,6 +990,7 @@ export default class MainBackground {
|
||||
|
||||
this.sendStateProvider = new SendStateProvider(this.stateProvider);
|
||||
this.sendService = new SendService(
|
||||
this.accountService,
|
||||
this.keyService,
|
||||
this.i18nService,
|
||||
this.keyGenerationService,
|
||||
@@ -999,7 +1006,6 @@ export default class MainBackground {
|
||||
this.avatarService = new AvatarService(this.apiService, this.stateProvider);
|
||||
|
||||
this.providerService = new ProviderService(this.stateProvider);
|
||||
|
||||
this.syncService = new DefaultSyncService(
|
||||
this.masterPasswordService,
|
||||
this.accountService,
|
||||
@@ -1025,6 +1031,7 @@ export default class MainBackground {
|
||||
this.tokenService,
|
||||
this.authService,
|
||||
this.stateProvider,
|
||||
this.securityStateService,
|
||||
);
|
||||
|
||||
this.syncServiceListener = new SyncServiceListener(
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<span>{{ "phishingPageLearnWhy"| i18n}}</span>
|
||||
<a href="http://bitwarden.com/help/phishing-blocked/" bitLink block buttonType="primary">
|
||||
{{ "learnMore" | i18n }}
|
||||
</a>
|
||||
@@ -1,13 +1,46 @@
|
||||
<div class="tw-flex tw-flex-col tw-gap-2">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "phishingPageTitle" | i18n }}</bit-label>
|
||||
<input bitInput disabled type="text" [value]="phishingHost" />
|
||||
</bit-form-field>
|
||||
<div class="tw-flex tw-flex-col">
|
||||
<div class="tw-flex tw-gap-4 tw-items-baseline">
|
||||
<bit-icon-tile size="large" icon="bwi-exclamation-triangle" variant="danger"></bit-icon-tile>
|
||||
<h1 bitTypography="h2" noMargin class="!tw-mb-0">{{ "phishingPageTitleV2" | i18n }}</h1>
|
||||
</div>
|
||||
|
||||
<button type="button" (click)="closeTab()" bitButton buttonType="primary">
|
||||
{{ "phishingPageCloseTab" | i18n }}
|
||||
</button>
|
||||
<button type="button" (click)="continueAnyway()" bitButton buttonType="danger">
|
||||
{{ "phishingPageContinue" | i18n }}
|
||||
</button>
|
||||
<hr class="!tw-mt-6 !tw-mb-4 !tw-border-secondary-100" />
|
||||
|
||||
<p bitTypography="body1">{{ "phishingPageSummary" | i18n }}</p>
|
||||
|
||||
<bit-callout class="tw-mb-0" type="danger" icon="bwi-globe" [title]="null">
|
||||
<span class="tw-font-mono">{{ phishingHost$ | async }}</span>
|
||||
</bit-callout>
|
||||
|
||||
<bit-callout class="tw-mt-2" [icon]="null" type="default">
|
||||
<p bitTypography="body2">
|
||||
{{ "phishingPageExplanation1" | i18n }}<b>Phishing.Database</b
|
||||
>{{ "phishingPageExplanation2" | i18n }}
|
||||
</p>
|
||||
|
||||
<a
|
||||
bitLink
|
||||
linkType="primary"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
href="https://bitwarden.com/help/phishing-blocked/"
|
||||
>
|
||||
{{ "phishingPageLearnMore" | i18n }}<i class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
</bit-callout>
|
||||
|
||||
<div class="tw-flex tw-flex-col tw-gap-4 tw-items-center tw-mt-2">
|
||||
<button type="button" (click)="closeTab()" bitButton buttonType="primary" [block]="true">
|
||||
{{ "phishingPageCloseTabV2" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
class="tw-text-sm"
|
||||
type="button"
|
||||
(click)="continueAnyway()"
|
||||
bitLink
|
||||
linkType="secondary"
|
||||
>
|
||||
{{ "phishingPageContinueV2" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CommonModule } from "@angular/common";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Component, OnDestroy } from "@angular/core";
|
||||
import { Component, inject } from "@angular/core";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { ActivatedRoute, RouterModule } from "@angular/router";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
import { map } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import {
|
||||
@@ -13,12 +13,16 @@ import {
|
||||
CheckboxModule,
|
||||
FormFieldModule,
|
||||
IconModule,
|
||||
IconTileComponent,
|
||||
LinkModule,
|
||||
CalloutComponent,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { PhishingDetectionService } from "../services/phishing-detection.service";
|
||||
|
||||
@Component({
|
||||
selector: "dirt-phishing-warning",
|
||||
standalone: true,
|
||||
templateUrl: "phishing-warning.component.html",
|
||||
imports: [
|
||||
@@ -31,18 +35,16 @@ import { PhishingDetectionService } from "../services/phishing-detection.service
|
||||
CheckboxModule,
|
||||
ButtonModule,
|
||||
RouterModule,
|
||||
IconTileComponent,
|
||||
CalloutComponent,
|
||||
TypographyModule,
|
||||
],
|
||||
})
|
||||
export class PhishingWarning implements OnDestroy {
|
||||
phishingHost = "";
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(private activatedRoute: ActivatedRoute) {
|
||||
this.activatedRoute.queryParamMap.pipe(takeUntil(this.destroy$)).subscribe((params) => {
|
||||
this.phishingHost = params.get("phishingHost") || "";
|
||||
});
|
||||
}
|
||||
export class PhishingWarning {
|
||||
private activatedRoute = inject(ActivatedRoute);
|
||||
protected phishingHost$ = this.activatedRoute.queryParamMap.pipe(
|
||||
map((params) => params.get("phishingHost") || ""),
|
||||
);
|
||||
|
||||
async closeTab() {
|
||||
await PhishingDetectionService.requestClosePhishingWarningPage();
|
||||
@@ -50,9 +52,4 @@ export class PhishingWarning implements OnDestroy {
|
||||
async continueAnyway() {
|
||||
await PhishingDetectionService.requestContinueToDangerousUrl();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
// TODO: This needs to be dealt with by moving this folder or updating the lint rule.
|
||||
/* eslint-disable no-restricted-imports */
|
||||
import { ActivatedRoute, RouterModule } from "@angular/router";
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { DeactivatedOrg } from "@bitwarden/assets/svg";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { AnonLayoutComponent, I18nMockService } from "@bitwarden/components";
|
||||
|
||||
import { PhishingWarning } from "./phishing-warning.component";
|
||||
import { ProtectedByComponent } from "./protected-by-component";
|
||||
|
||||
class MockPlatformUtilsService implements Partial<PlatformUtilsService> {
|
||||
getApplicationVersion = () => Promise.resolve("Version 2024.1.1");
|
||||
getClientType = () => ClientType.Web;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create ActivatedRoute mock with query parameters
|
||||
*/
|
||||
function mockActivatedRoute(queryParams: Record<string, string>) {
|
||||
return {
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
queryParamMap: of({
|
||||
get: (key: string) => queryParams[key] || null,
|
||||
}),
|
||||
queryParams: of(queryParams),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
type StoryArgs = {
|
||||
phishingHost: string;
|
||||
};
|
||||
|
||||
export default {
|
||||
title: "Browser/DIRT/Phishing Warning",
|
||||
component: PhishingWarning,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [AnonLayoutComponent, ProtectedByComponent, RouterModule],
|
||||
providers: [
|
||||
{
|
||||
provide: PlatformUtilsService,
|
||||
useClass: MockPlatformUtilsService,
|
||||
},
|
||||
{
|
||||
provide: I18nService,
|
||||
useFactory: () =>
|
||||
new I18nMockService({
|
||||
accessing: "Accessing",
|
||||
appLogoLabel: "Bitwarden logo",
|
||||
phishingPageTitleV2: "Phishing attempt detected",
|
||||
phishingPageCloseTabV2: "Close this tab",
|
||||
phishingPageSummary:
|
||||
"The site you are attempting to visit is a known malicious site and a security risk.",
|
||||
phishingPageContinueV2: "Continue to this site (not recommended)",
|
||||
phishingPageExplanation1: "This site was found in ",
|
||||
phishingPageExplanation2:
|
||||
", an open-source list of known phishing sites used for stealing personal and sensitive information.",
|
||||
phishingPageLearnMore: "Learn more about phishing detection",
|
||||
protectedBy: (product) => `Protected by ${product}`,
|
||||
learnMore: "Learn more",
|
||||
danger: "error",
|
||||
}),
|
||||
},
|
||||
{
|
||||
provide: EnvironmentService,
|
||||
useValue: {
|
||||
environment$: new BehaviorSubject({
|
||||
getHostname() {
|
||||
return "bitwarden.com";
|
||||
},
|
||||
}).asObservable(),
|
||||
},
|
||||
},
|
||||
mockActivatedRoute({ phishingHost: "malicious-example.com" }),
|
||||
],
|
||||
}),
|
||||
],
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: /*html*/ `
|
||||
<auth-anon-layout
|
||||
[hideIcon]="true"
|
||||
[hideBackgroundIllustration]="true"
|
||||
>
|
||||
<dirt-phishing-warning></dirt-phishing-warning>
|
||||
<dirt-phishing-protected-by slot="secondary"></dirt-phishing-protected-by>
|
||||
</auth-anon-layout>
|
||||
`,
|
||||
}),
|
||||
argTypes: {
|
||||
phishingHost: {
|
||||
control: "text",
|
||||
description: "The suspicious host that was blocked",
|
||||
},
|
||||
},
|
||||
args: {
|
||||
phishingHost: "malicious-example.com",
|
||||
pageIcon: DeactivatedOrg,
|
||||
},
|
||||
} satisfies Meta<StoryArgs & { pageIcon: any }>;
|
||||
|
||||
type Story = StoryObj<StoryArgs & { pageIcon: any }>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
phishingHost: "malicious-example.com",
|
||||
},
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
providers: [mockActivatedRoute({ phishingHost: "malicious-example.com" })],
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
export const LongHostname: Story = {
|
||||
args: {
|
||||
phishingHost: "very-long-suspicious-phishing-domain-name-that-might-wrap.malicious-example.com",
|
||||
},
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
providers: [
|
||||
mockActivatedRoute({
|
||||
phishingHost:
|
||||
"very-long-suspicious-phishing-domain-name-that-might-wrap.malicious-example.com",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
<span class="tw-text-muted">{{ "protectedBy" | i18n: "Bitwarden Phishing Blocker" }}</span>
|
||||
@@ -4,13 +4,12 @@ import { CommonModule } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { ButtonModule } from "@bitwarden/components";
|
||||
import { ButtonModule, LinkModule } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
selector: "dirt-phishing-protected-by",
|
||||
standalone: true,
|
||||
templateUrl: "learn-more-component.html",
|
||||
imports: [CommonModule, CommonModule, JslibModule, ButtonModule],
|
||||
templateUrl: "protected-by-component.html",
|
||||
imports: [CommonModule, CommonModule, JslibModule, ButtonModule, LinkModule],
|
||||
})
|
||||
export class LearnMoreComponent {
|
||||
constructor() {}
|
||||
}
|
||||
export class ProtectedByComponent {}
|
||||
@@ -116,15 +116,15 @@ export class PhishingDetectionService {
|
||||
/**
|
||||
* Sends a message to the phishing detection service to close the warning page
|
||||
*/
|
||||
static requestClosePhishingWarningPage(): void {
|
||||
void chrome.runtime.sendMessage({ command: PhishingDetectionMessage.Close });
|
||||
static async requestClosePhishingWarningPage() {
|
||||
await chrome.runtime.sendMessage({ command: PhishingDetectionMessage.Close });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to the phishing detection service to continue to the caught url
|
||||
*/
|
||||
static async requestContinueToDangerousUrl() {
|
||||
void chrome.runtime.sendMessage({ command: PhishingDetectionMessage.Continue });
|
||||
await chrome.runtime.sendMessage({ command: PhishingDetectionMessage.Continue });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,6 +29,9 @@ import {
|
||||
SearchModule,
|
||||
SectionComponent,
|
||||
ScrollLayoutDirective,
|
||||
SkeletonComponent,
|
||||
SkeletonTextComponent,
|
||||
SkeletonGroupComponent,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { PopupRouterCacheService } from "../view-cache/popup-router-cache.service";
|
||||
@@ -335,6 +338,9 @@ export default {
|
||||
SectionComponent,
|
||||
IconButtonModule,
|
||||
BadgeModule,
|
||||
SkeletonComponent,
|
||||
SkeletonTextComponent,
|
||||
SkeletonGroupComponent,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
@@ -594,6 +600,34 @@ export const Loading: Story = {
|
||||
}),
|
||||
};
|
||||
|
||||
export const SkeletonLoading: Story = {
|
||||
render: (args) => ({
|
||||
props: { ...args, data: Array(8) },
|
||||
template: /* HTML */ `
|
||||
<extension-container>
|
||||
<popup-tab-navigation>
|
||||
<popup-page>
|
||||
<popup-header slot="header" pageTitle="Page Header"></popup-header>
|
||||
<div>
|
||||
<div class="tw-sr-only" role="status">Loading...</div>
|
||||
<div class="tw-flex tw-flex-col tw-gap-4">
|
||||
<bit-skeleton-text class="tw-w-1/3"></bit-skeleton-text>
|
||||
@for (num of data; track $index) {
|
||||
<bit-skeleton-group>
|
||||
<bit-skeleton class="tw-size-8" slot="start"></bit-skeleton>
|
||||
<bit-skeleton-text [lines]="2" class="tw-w-1/2"></bit-skeleton-text>
|
||||
</bit-skeleton-group>
|
||||
<bit-skeleton class="tw-w-full tw-h-[1px]"></bit-skeleton>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</popup-page>
|
||||
</popup-tab-navigation>
|
||||
</extension-container>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const TransparentHeader: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
VaultIcon,
|
||||
LockIcon,
|
||||
TwoFactorAuthSecurityKeyIcon,
|
||||
DeactivatedOrg,
|
||||
} from "@bitwarden/assets/svg";
|
||||
import {
|
||||
LoginComponent,
|
||||
@@ -54,8 +53,8 @@ import { BlockedDomainsComponent } from "../autofill/popup/settings/blocked-doma
|
||||
import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component";
|
||||
import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component";
|
||||
import { PremiumV2Component } from "../billing/popup/settings/premium-v2.component";
|
||||
import { LearnMoreComponent } from "../dirt/phishing-detection/pages/learn-more-component";
|
||||
import { PhishingWarning } from "../dirt/phishing-detection/pages/phishing-warning.component";
|
||||
import { ProtectedByComponent } from "../dirt/phishing-detection/pages/protected-by-component";
|
||||
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
|
||||
import BrowserPopupUtils from "../platform/browser/browser-popup-utils";
|
||||
import { popupRouterCacheGuard } from "../platform/popup/view-cache/popup-router-cache.service";
|
||||
@@ -718,14 +717,13 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: LearnMoreComponent,
|
||||
component: ProtectedByComponent,
|
||||
outlet: "secondary",
|
||||
},
|
||||
],
|
||||
data: {
|
||||
pageIcon: DeactivatedOrg,
|
||||
pageTitle: "Bitwarden blocked it!",
|
||||
pageSubtitle: "Bitwarden blocked a known phishing site from loading.",
|
||||
hideIcon: true,
|
||||
hideBackgroundIllustration: true,
|
||||
showReadonlyHostname: true,
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
},
|
||||
|
||||
@@ -382,7 +382,7 @@ app-root {
|
||||
}
|
||||
}
|
||||
|
||||
main:not(popup-page main) {
|
||||
main:not(popup-page main):not(auth-anon-layout main) {
|
||||
position: absolute;
|
||||
top: 44px;
|
||||
bottom: 0;
|
||||
|
||||
@@ -302,7 +302,7 @@ export class ItemMoreOptionsComponent {
|
||||
await this.cipherArchiveService.archiveWithServer(this.cipher.id as CipherId, activeUserId);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
message: this.i18nService.t("itemSentToArchive"),
|
||||
message: this.i18nService.t("itemWasSentToArchive"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
{{ "clone" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem (click)="unarchive(cipher)">
|
||||
{{ "unarchive" | i18n }}
|
||||
{{ "unArchive" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -133,7 +133,7 @@ export class ArchiveComponent {
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
message: this.i18nService.t("itemRemovedFromArchive"),
|
||||
message: this.i18nService.t("itemUnarchived"),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -10,14 +10,18 @@ const configurator = require("./config/config");
|
||||
const manifest = require("./webpack/manifest");
|
||||
const AngularCheckPlugin = require("./webpack/angular-check");
|
||||
|
||||
module.exports.getEnv = function getEnv() {
|
||||
const ENV = (process.env.ENV = process.env.NODE_ENV);
|
||||
module.exports.getEnv = function getEnv(params) {
|
||||
const ENV = params.env || (process.env.ENV = process.env.NODE_ENV);
|
||||
const manifestVersion = process.env.MANIFEST_VERSION == 3 ? 3 : 2;
|
||||
const browser = process.env.BROWSER ?? "chrome";
|
||||
|
||||
return { ENV, manifestVersion, browser };
|
||||
};
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
outputPath: path.resolve(__dirname, "build"),
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* configName: string;
|
||||
@@ -29,15 +33,20 @@ module.exports.getEnv = function getEnv() {
|
||||
* entry: string;
|
||||
* };
|
||||
* tsConfig: string;
|
||||
* outputPath?: string;
|
||||
* mode?: string;
|
||||
* env?: string;
|
||||
* additionalEntries?: { [outputPath: string]: string }
|
||||
* }} params - The input parameters for building the config.
|
||||
*/
|
||||
module.exports.buildConfig = function buildConfig(params) {
|
||||
params = { ...DEFAULT_PARAMS, ...params };
|
||||
|
||||
if (process.env.NODE_ENV == null) {
|
||||
process.env.NODE_ENV = "development";
|
||||
}
|
||||
|
||||
const { ENV, manifestVersion, browser } = module.exports.getEnv();
|
||||
const { ENV, manifestVersion, browser } = module.exports.getEnv(params);
|
||||
|
||||
console.log(`Building Manifest Version ${manifestVersion} app - ${params.configName} version`);
|
||||
|
||||
@@ -103,7 +112,7 @@ module.exports.buildConfig = function buildConfig(params) {
|
||||
{
|
||||
loader: "babel-loader",
|
||||
options: {
|
||||
configFile: "../../babel.config.json",
|
||||
configFile: path.resolve(__dirname, "../../babel.config.json"),
|
||||
cacheDirectory: ENV === "development",
|
||||
compact: ENV !== "development",
|
||||
},
|
||||
@@ -130,43 +139,52 @@ module.exports.buildConfig = function buildConfig(params) {
|
||||
|
||||
const plugins = [
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/popup/index.ejs",
|
||||
template: path.resolve(__dirname, "src/popup/index.ejs"),
|
||||
filename: "popup/index.html",
|
||||
chunks: ["popup/polyfills", "popup/vendor-angular", "popup/vendor", "popup/main"],
|
||||
browser: browser,
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/autofill/notification/bar.html",
|
||||
template: path.resolve(__dirname, "src/autofill/notification/bar.html"),
|
||||
filename: "notification/bar.html",
|
||||
chunks: ["notification/bar"],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/autofill/overlay/inline-menu/pages/button/button.html",
|
||||
template: path.resolve(
|
||||
__dirname,
|
||||
"src/autofill/overlay/inline-menu/pages/button/button.html",
|
||||
),
|
||||
filename: "overlay/menu-button.html",
|
||||
chunks: ["overlay/menu-button"],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/autofill/overlay/inline-menu/pages/list/list.html",
|
||||
template: path.resolve(__dirname, "src/autofill/overlay/inline-menu/pages/list/list.html"),
|
||||
filename: "overlay/menu-list.html",
|
||||
chunks: ["overlay/menu-list"],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/autofill/overlay/inline-menu/pages/menu-container/menu-container.html",
|
||||
template: path.resolve(
|
||||
__dirname,
|
||||
"src/autofill/overlay/inline-menu/pages/menu-container/menu-container.html",
|
||||
),
|
||||
filename: "overlay/menu.html",
|
||||
chunks: ["overlay/menu"],
|
||||
}),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: manifestVersion == 3 ? "./src/manifest.v3.json" : "./src/manifest.json",
|
||||
from:
|
||||
manifestVersion == 3
|
||||
? path.resolve(__dirname, "src/manifest.v3.json")
|
||||
: path.resolve(__dirname, "src/manifest.json"),
|
||||
to: "manifest.json",
|
||||
transform: manifest.transform(browser),
|
||||
},
|
||||
{ from: "./src/managed_schema.json", to: "managed_schema.json" },
|
||||
{ from: "./src/_locales", to: "_locales" },
|
||||
{ from: "./src/images", to: "images" },
|
||||
{ from: "./src/popup/images", to: "popup/images" },
|
||||
{ from: "./src/autofill/content/autofill.css", to: "content" },
|
||||
{ from: path.resolve(__dirname, "src/managed_schema.json"), to: "managed_schema.json" },
|
||||
{ from: path.resolve(__dirname, "src/_locales"), to: "_locales" },
|
||||
{ from: path.resolve(__dirname, "src/images"), to: "images" },
|
||||
{ from: path.resolve(__dirname, "src/popup/images"), to: "popup/images" },
|
||||
{ from: path.resolve(__dirname, "src/autofill/content/autofill.css"), to: "content" },
|
||||
],
|
||||
}),
|
||||
new MiniCssExtractPlugin({
|
||||
@@ -196,33 +214,76 @@ module.exports.buildConfig = function buildConfig(params) {
|
||||
name: "main",
|
||||
mode: ENV,
|
||||
devtool: false,
|
||||
|
||||
entry: {
|
||||
"popup/polyfills": "./src/popup/polyfills.ts",
|
||||
"popup/polyfills": path.resolve(__dirname, "src/popup/polyfills.ts"),
|
||||
"popup/main": params.popup.entry,
|
||||
"content/trigger-autofill-script-injection":
|
||||
"./src/autofill/content/trigger-autofill-script-injection.ts",
|
||||
"content/bootstrap-autofill": "./src/autofill/content/bootstrap-autofill.ts",
|
||||
"content/bootstrap-autofill-overlay": "./src/autofill/content/bootstrap-autofill-overlay.ts",
|
||||
"content/bootstrap-autofill-overlay-menu":
|
||||
"./src/autofill/content/bootstrap-autofill-overlay-menu.ts",
|
||||
"content/bootstrap-autofill-overlay-notifications":
|
||||
"./src/autofill/content/bootstrap-autofill-overlay-notifications.ts",
|
||||
"content/autofiller": "./src/autofill/content/autofiller.ts",
|
||||
"content/auto-submit-login": "./src/autofill/content/auto-submit-login.ts",
|
||||
"content/contextMenuHandler": "./src/autofill/content/context-menu-handler.ts",
|
||||
"content/content-message-handler": "./src/autofill/content/content-message-handler.ts",
|
||||
"content/fido2-content-script": "./src/autofill/fido2/content/fido2-content-script.ts",
|
||||
"content/fido2-page-script": "./src/autofill/fido2/content/fido2-page-script.ts",
|
||||
"content/ipc-content-script": "./src/platform/ipc/content/ipc-content-script.ts",
|
||||
"notification/bar": "./src/autofill/notification/bar.ts",
|
||||
"overlay/menu-button":
|
||||
"./src/autofill/overlay/inline-menu/pages/button/bootstrap-autofill-inline-menu-button.ts",
|
||||
"overlay/menu-list":
|
||||
"./src/autofill/overlay/inline-menu/pages/list/bootstrap-autofill-inline-menu-list.ts",
|
||||
"overlay/menu":
|
||||
"./src/autofill/overlay/inline-menu/pages/menu-container/bootstrap-autofill-inline-menu-container.ts",
|
||||
"content/send-on-installed-message": "./src/vault/content/send-on-installed-message.ts",
|
||||
"content/send-popup-open-message": "./src/vault/content/send-popup-open-message.ts",
|
||||
"content/trigger-autofill-script-injection": path.resolve(
|
||||
__dirname,
|
||||
"src/autofill/content/trigger-autofill-script-injection.ts",
|
||||
),
|
||||
"content/bootstrap-autofill": path.resolve(
|
||||
__dirname,
|
||||
"src/autofill/content/bootstrap-autofill.ts",
|
||||
),
|
||||
"content/bootstrap-autofill-overlay": path.resolve(
|
||||
__dirname,
|
||||
"src/autofill/content/bootstrap-autofill-overlay.ts",
|
||||
),
|
||||
"content/bootstrap-autofill-overlay-menu": path.resolve(
|
||||
__dirname,
|
||||
"src/autofill/content/bootstrap-autofill-overlay-menu.ts",
|
||||
),
|
||||
"content/bootstrap-autofill-overlay-notifications": path.resolve(
|
||||
__dirname,
|
||||
"src/autofill/content/bootstrap-autofill-overlay-notifications.ts",
|
||||
),
|
||||
"content/autofiller": path.resolve(__dirname, "src/autofill/content/autofiller.ts"),
|
||||
"content/auto-submit-login": path.resolve(
|
||||
__dirname,
|
||||
"src/autofill/content/auto-submit-login.ts",
|
||||
),
|
||||
"content/contextMenuHandler": path.resolve(
|
||||
__dirname,
|
||||
"src/autofill/content/context-menu-handler.ts",
|
||||
),
|
||||
"content/content-message-handler": path.resolve(
|
||||
__dirname,
|
||||
"src/autofill/content/content-message-handler.ts",
|
||||
),
|
||||
"content/fido2-content-script": path.resolve(
|
||||
__dirname,
|
||||
"src/autofill/fido2/content/fido2-content-script.ts",
|
||||
),
|
||||
"content/fido2-page-script": path.resolve(
|
||||
__dirname,
|
||||
"src/autofill/fido2/content/fido2-page-script.ts",
|
||||
),
|
||||
"content/ipc-content-script": path.resolve(
|
||||
__dirname,
|
||||
"src/platform/ipc/content/ipc-content-script.ts",
|
||||
),
|
||||
"notification/bar": path.resolve(__dirname, "src/autofill/notification/bar.ts"),
|
||||
"overlay/menu-button": path.resolve(
|
||||
__dirname,
|
||||
"src/autofill/overlay/inline-menu/pages/button/bootstrap-autofill-inline-menu-button.ts",
|
||||
),
|
||||
"overlay/menu-list": path.resolve(
|
||||
__dirname,
|
||||
"src/autofill/overlay/inline-menu/pages/list/bootstrap-autofill-inline-menu-list.ts",
|
||||
),
|
||||
"overlay/menu": path.resolve(
|
||||
__dirname,
|
||||
"src/autofill/overlay/inline-menu/pages/menu-container/bootstrap-autofill-inline-menu-container.ts",
|
||||
),
|
||||
"content/send-on-installed-message": path.resolve(
|
||||
__dirname,
|
||||
"src/vault/content/send-on-installed-message.ts",
|
||||
),
|
||||
"content/send-popup-open-message": path.resolve(
|
||||
__dirname,
|
||||
"src/vault/content/send-popup-open-message.ts",
|
||||
),
|
||||
...params.additionalEntries,
|
||||
},
|
||||
cache:
|
||||
@@ -291,7 +352,7 @@ module.exports.buildConfig = function buildConfig(params) {
|
||||
resolve: {
|
||||
extensions: [".ts", ".js"],
|
||||
symlinks: false,
|
||||
modules: [path.resolve("../../node_modules")],
|
||||
modules: [path.resolve(__dirname, "../../node_modules")],
|
||||
fallback: {
|
||||
assert: false,
|
||||
buffer: require.resolve("buffer/"),
|
||||
@@ -306,7 +367,7 @@ module.exports.buildConfig = function buildConfig(params) {
|
||||
filename: "[name].js",
|
||||
chunkFilename: "assets/[name].js",
|
||||
webassemblyModuleFilename: "assets/[modulehash].wasm",
|
||||
path: path.resolve(__dirname, "build"),
|
||||
path: params.outputPath,
|
||||
clean: true,
|
||||
},
|
||||
module: {
|
||||
@@ -335,7 +396,7 @@ module.exports.buildConfig = function buildConfig(params) {
|
||||
// Manifest V2 uses Background Pages which requires a html page.
|
||||
mainConfig.plugins.push(
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/platform/background.html",
|
||||
template: path.resolve(__dirname, "src/platform/background.html"),
|
||||
filename: "background.html",
|
||||
chunks: ["vendor", "background"],
|
||||
}),
|
||||
@@ -344,19 +405,23 @@ module.exports.buildConfig = function buildConfig(params) {
|
||||
// Manifest V2 background pages can be run through the regular build pipeline.
|
||||
// Since it's a standard webpage.
|
||||
mainConfig.entry.background = params.background.entry;
|
||||
mainConfig.entry["content/fido2-page-script-delay-append-mv2"] =
|
||||
"./src/autofill/fido2/content/fido2-page-script-delay-append.mv2.ts";
|
||||
mainConfig.entry["content/fido2-page-script-delay-append-mv2"] = path.resolve(
|
||||
__dirname,
|
||||
"src/autofill/fido2/content/fido2-page-script-delay-append.mv2.ts",
|
||||
);
|
||||
|
||||
configs.push(mainConfig);
|
||||
} else {
|
||||
// Firefox does not use the offscreen API
|
||||
if (browser !== "firefox") {
|
||||
mainConfig.entry["offscreen-document/offscreen-document"] =
|
||||
"./src/platform/offscreen-document/offscreen-document.ts";
|
||||
mainConfig.entry["offscreen-document/offscreen-document"] = path.resolve(
|
||||
__dirname,
|
||||
"src/platform/offscreen-document/offscreen-document.ts",
|
||||
);
|
||||
|
||||
mainConfig.plugins.push(
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/platform/offscreen-document/index.html",
|
||||
template: path.resolve(__dirname, "src/platform/offscreen-document/index.html"),
|
||||
filename: "offscreen-document/index.html",
|
||||
chunks: ["offscreen-document/offscreen-document"],
|
||||
}),
|
||||
@@ -372,11 +437,12 @@ module.exports.buildConfig = function buildConfig(params) {
|
||||
name: "background",
|
||||
mode: ENV,
|
||||
devtool: false,
|
||||
|
||||
entry: params.background.entry,
|
||||
target: target,
|
||||
output: {
|
||||
filename: "background.js",
|
||||
path: path.resolve(__dirname, "build"),
|
||||
path: params.outputPath,
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
@@ -409,7 +475,7 @@ module.exports.buildConfig = function buildConfig(params) {
|
||||
resolve: {
|
||||
extensions: [".ts", ".js"],
|
||||
symlinks: false,
|
||||
modules: [path.resolve("../../node_modules")],
|
||||
modules: [path.resolve(__dirname, "../../node_modules")],
|
||||
plugins: [new TsconfigPathsPlugin()],
|
||||
fallback: {
|
||||
fs: false,
|
||||
@@ -428,8 +494,11 @@ module.exports.buildConfig = function buildConfig(params) {
|
||||
backgroundConfig.plugins.push(
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{ from: "./src/safari/mv3/fake-background.html", to: "background.html" },
|
||||
{ from: "./src/safari/mv3/fake-vendor.js", to: "vendor.js" },
|
||||
{
|
||||
from: path.resolve(__dirname, "src/safari/mv3/fake-background.html"),
|
||||
to: "background.html",
|
||||
},
|
||||
{ from: path.resolve(__dirname, "src/safari/mv3/fake-vendor.js"), to: "vendor.js" },
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,13 +1,54 @@
|
||||
const path = require("path");
|
||||
const { buildConfig } = require("./webpack.base");
|
||||
|
||||
module.exports = buildConfig({
|
||||
configName: "OSS",
|
||||
popup: {
|
||||
entry: "./src/popup/main.ts",
|
||||
entryModule: "src/popup/app.module#AppModule",
|
||||
},
|
||||
background: {
|
||||
entry: "./src/platform/background.ts",
|
||||
},
|
||||
tsConfig: "tsconfig.json",
|
||||
});
|
||||
module.exports = (webpackConfig, context) => {
|
||||
// Detect if called by Nx (context parameter exists)
|
||||
const isNxBuild = context && context.options;
|
||||
|
||||
if (isNxBuild) {
|
||||
// Nx build configuration
|
||||
const mode = context.options.mode || "development";
|
||||
if (process.env.NODE_ENV == null) {
|
||||
process.env.NODE_ENV = mode;
|
||||
}
|
||||
const ENV = (process.env.ENV = process.env.NODE_ENV);
|
||||
|
||||
// Set environment variables from Nx context
|
||||
if (context.options.env) {
|
||||
Object.keys(context.options.env).forEach((key) => {
|
||||
process.env[key] = context.options.env[key];
|
||||
});
|
||||
}
|
||||
|
||||
return buildConfig({
|
||||
configName: "OSS",
|
||||
popup: {
|
||||
entry: path.resolve(__dirname, "src/popup/main.ts"),
|
||||
entryModule: "src/popup/app.module#AppModule",
|
||||
},
|
||||
background: {
|
||||
entry: path.resolve(__dirname, "src/platform/background.ts"),
|
||||
},
|
||||
tsConfig: path.resolve(__dirname, "tsconfig.json"),
|
||||
outputPath:
|
||||
context.context && context.context.root
|
||||
? path.resolve(context.context.root, context.options.outputPath)
|
||||
: context.options.outputPath,
|
||||
mode: mode,
|
||||
env: ENV,
|
||||
});
|
||||
} else {
|
||||
// npm build configuration
|
||||
return buildConfig({
|
||||
configName: "OSS",
|
||||
popup: {
|
||||
entry: path.resolve(__dirname, "src/popup/main.ts"),
|
||||
entryModule: "src/popup/app.module#AppModule",
|
||||
},
|
||||
background: {
|
||||
entry: path.resolve(__dirname, "src/platform/background.ts"),
|
||||
},
|
||||
tsConfig: "tsconfig.json",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
SsoUrlService,
|
||||
UserApiLoginCredentials,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
@@ -29,6 +28,7 @@ import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/ide
|
||||
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
|
||||
import { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two-factor-email.request";
|
||||
import { UpdateTempPasswordRequest } from "@bitwarden/common/auth/models/request/update-temp-password.request";
|
||||
import { TwoFactorApiService } from "@bitwarden/common/auth/two-factor";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
@@ -62,7 +62,7 @@ export class LoginCommand {
|
||||
constructor(
|
||||
protected loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
protected authService: AuthService,
|
||||
protected apiService: ApiService,
|
||||
protected twoFactorApiService: TwoFactorApiService,
|
||||
protected masterPasswordApiService: MasterPasswordApiService,
|
||||
protected cryptoFunctionService: CryptoFunctionService,
|
||||
protected environmentService: EnvironmentService,
|
||||
@@ -279,7 +279,7 @@ export class LoginCommand {
|
||||
const emailReq = new TwoFactorEmailRequest();
|
||||
emailReq.email = await this.loginStrategyService.getEmail();
|
||||
emailReq.masterPasswordHash = await this.loginStrategyService.getMasterPasswordHash();
|
||||
await this.apiService.postTwoFactorEmail(emailReq);
|
||||
await this.twoFactorApiService.postTwoFactorEmail(emailReq);
|
||||
}
|
||||
|
||||
if (twoFactorToken == null) {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { UnlockCommand } from "./auth/commands/unlock.command";
|
||||
import { UnlockCommand } from "./key-management/commands/unlock.command";
|
||||
import { Response } from "./models/response";
|
||||
import { ListResponse } from "./models/response/list.response";
|
||||
import { MessageResponse } from "./models/response/message.response";
|
||||
@@ -182,6 +182,8 @@ export abstract class BaseProgram {
|
||||
this.serviceContainer.organizationApiService,
|
||||
this.serviceContainer.logout,
|
||||
this.serviceContainer.i18nService,
|
||||
this.serviceContainer.masterPasswordUnlockService,
|
||||
this.serviceContainer.configService,
|
||||
);
|
||||
const response = await command.run(null, null);
|
||||
if (!response.success) {
|
||||
|
||||
@@ -261,8 +261,13 @@ export class EditCommand {
|
||||
|
||||
/** Prompt the user to accept movement of their cipher back to the their vault. */
|
||||
private async promptForArchiveEdit(): Promise<boolean> {
|
||||
// When running in serve or no interaction mode, automatically accept the prompt
|
||||
if (process.env.BW_SERVE === "true" || process.env.BW_NOINTERACTION === "true") {
|
||||
// When user has disabled interactivity or does not have the ability to prompt,
|
||||
// automatically move the item back to the vault and inform them.
|
||||
if (
|
||||
process.env.BW_SERVE === "true" ||
|
||||
process.env.BW_NOINTERACTION === "true" ||
|
||||
!process.stdin.isTTY
|
||||
) {
|
||||
CliUtils.writeLn(
|
||||
"Archive is only available with a Premium subscription, which has ended. Your edit was saved and the item was moved back to your vault.",
|
||||
);
|
||||
|
||||
318
apps/cli/src/key-management/commands/unlock.command.spec.ts
Normal file
318
apps/cli/src/key-management/commands/unlock.command.spec.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
||||
import { MasterPasswordVerificationResponse } from "@bitwarden/common/auth/types/verification";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { MasterPasswordUnlockService } from "@bitwarden/common/key-management/master-password/abstractions/master-password-unlock.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { ConsoleLogService } from "@bitwarden/logging";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
import { MessageResponse } from "../../models/response/message.response";
|
||||
import { I18nService } from "../../platform/services/i18n.service";
|
||||
import { ConvertToKeyConnectorCommand } from "../convert-to-key-connector.command";
|
||||
|
||||
import { UnlockCommand } from "./unlock.command";
|
||||
|
||||
describe("UnlockCommand", () => {
|
||||
let command: UnlockCommand;
|
||||
|
||||
const accountService = mock<AccountService>();
|
||||
const masterPasswordService = mock<InternalMasterPasswordServiceAbstraction>();
|
||||
const keyService = mock<KeyService>();
|
||||
const userVerificationService = mock<UserVerificationService>();
|
||||
const cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
const logService = mock<ConsoleLogService>();
|
||||
const keyConnectorService = mock<KeyConnectorService>();
|
||||
const environmentService = mock<EnvironmentService>();
|
||||
const organizationApiService = mock<OrganizationApiServiceAbstraction>();
|
||||
const logout = jest.fn();
|
||||
const i18nService = mock<I18nService>();
|
||||
const masterPasswordUnlockService = mock<MasterPasswordUnlockService>();
|
||||
const configService = mock<ConfigService>();
|
||||
|
||||
const mockMasterPassword = "testExample";
|
||||
const activeAccount: Account = {
|
||||
id: "user-id" as UserId,
|
||||
email: "user@example.com",
|
||||
emailVerified: true,
|
||||
name: "User",
|
||||
};
|
||||
const mockUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey;
|
||||
const mockSessionKey = new Uint8Array(64) as CsprngArray;
|
||||
const b64sessionKey = Utils.fromBufferToB64(mockSessionKey);
|
||||
const expectedSuccessMessage = new MessageResponse(
|
||||
"Your vault is now unlocked!",
|
||||
"\n" +
|
||||
"To unlock your vault, set your session key to the `BW_SESSION` environment variable. ex:\n" +
|
||||
'$ export BW_SESSION="' +
|
||||
b64sessionKey +
|
||||
'"\n' +
|
||||
'> $env:BW_SESSION="' +
|
||||
b64sessionKey +
|
||||
'"\n\n' +
|
||||
"You can also pass the session key to any command with the `--session` option. ex:\n" +
|
||||
"$ bw list items --session " +
|
||||
b64sessionKey,
|
||||
);
|
||||
expectedSuccessMessage.raw = b64sessionKey;
|
||||
|
||||
// Legacy test data
|
||||
const mockMasterKey = new SymmetricCryptoKey(new Uint8Array(32)) as MasterKey;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
i18nService.t.mockImplementation((key: string) => key);
|
||||
accountService.activeAccount$ = of(activeAccount);
|
||||
keyConnectorService.convertAccountRequired$ = of(false);
|
||||
cryptoFunctionService.randomBytes.mockResolvedValue(mockSessionKey);
|
||||
|
||||
command = new UnlockCommand(
|
||||
accountService,
|
||||
masterPasswordService,
|
||||
keyService,
|
||||
userVerificationService,
|
||||
cryptoFunctionService,
|
||||
logService,
|
||||
keyConnectorService,
|
||||
environmentService,
|
||||
organizationApiService,
|
||||
logout,
|
||||
i18nService,
|
||||
masterPasswordUnlockService,
|
||||
configService,
|
||||
);
|
||||
});
|
||||
|
||||
describe("run", () => {
|
||||
test.each([null as unknown as Account, undefined as unknown as Account])(
|
||||
"returns error response when the active account is %s",
|
||||
async (account) => {
|
||||
accountService.activeAccount$ = of(account);
|
||||
|
||||
const response = await command.run(mockMasterPassword, {});
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.success).toEqual(false);
|
||||
expect(response.message).toEqual("No active account found");
|
||||
expect(keyService.setUserKey).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
test.each([null as unknown as string, undefined as unknown as string, ""])(
|
||||
"returns error response when the provided password is %s",
|
||||
async (mockMasterPassword) => {
|
||||
process.env.BW_NOINTERACTION = "true";
|
||||
|
||||
const response = await command.run(mockMasterPassword, {});
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.success).toEqual(false);
|
||||
expect(response.message).toEqual(
|
||||
"Master password is required. Try again in interactive mode or provide a password file or environment variable.",
|
||||
);
|
||||
expect(keyService.setUserKey).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
describe("UnlockWithMasterPasswordUnlockData feature flag enabled", () => {
|
||||
beforeEach(() => {
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
});
|
||||
|
||||
it("calls masterPasswordUnlockService successfully", async () => {
|
||||
masterPasswordUnlockService.unlockWithMasterPassword.mockResolvedValue(mockUserKey);
|
||||
|
||||
const response = await command.run(mockMasterPassword, {});
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.success).toEqual(true);
|
||||
expect(response.data).toEqual(expectedSuccessMessage);
|
||||
expect(masterPasswordUnlockService.unlockWithMasterPassword).toHaveBeenCalledWith(
|
||||
mockMasterPassword,
|
||||
activeAccount.id,
|
||||
);
|
||||
expect(keyService.setUserKey).toHaveBeenCalledWith(mockUserKey, activeAccount.id);
|
||||
});
|
||||
|
||||
it("returns error response if unlockWithMasterPassword fails", async () => {
|
||||
masterPasswordUnlockService.unlockWithMasterPassword.mockRejectedValue(
|
||||
new Error("Unlock failed"),
|
||||
);
|
||||
|
||||
const response = await command.run(mockMasterPassword, {});
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.success).toEqual(false);
|
||||
expect(response.message).toEqual("Unlock failed");
|
||||
expect(masterPasswordUnlockService.unlockWithMasterPassword).toHaveBeenCalledWith(
|
||||
mockMasterPassword,
|
||||
activeAccount.id,
|
||||
);
|
||||
expect(keyService.setUserKey).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("unlock with feature flag off", () => {
|
||||
beforeEach(() => {
|
||||
configService.getFeatureFlag$.mockReturnValue(of(false));
|
||||
});
|
||||
|
||||
it("calls decryptUserKeyWithMasterKey successfully", async () => {
|
||||
userVerificationService.verifyUserByMasterPassword.mockResolvedValue({
|
||||
masterKey: mockMasterKey,
|
||||
} as MasterPasswordVerificationResponse);
|
||||
masterPasswordService.decryptUserKeyWithMasterKey.mockResolvedValue(mockUserKey);
|
||||
|
||||
const response = await command.run(mockMasterPassword, {});
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.success).toEqual(true);
|
||||
expect(response.data).toEqual(expectedSuccessMessage);
|
||||
expect(userVerificationService.verifyUserByMasterPassword).toHaveBeenCalledWith(
|
||||
{
|
||||
type: VerificationType.MasterPassword,
|
||||
secret: mockMasterPassword,
|
||||
},
|
||||
activeAccount.id,
|
||||
activeAccount.email,
|
||||
);
|
||||
expect(masterPasswordService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(
|
||||
mockMasterKey,
|
||||
activeAccount.id,
|
||||
);
|
||||
expect(keyService.setUserKey).toHaveBeenCalledWith(mockUserKey, activeAccount.id);
|
||||
});
|
||||
|
||||
it("returns error response when verifyUserByMasterPassword throws", async () => {
|
||||
userVerificationService.verifyUserByMasterPassword.mockRejectedValue(
|
||||
new Error("Verification failed"),
|
||||
);
|
||||
|
||||
const response = await command.run(mockMasterPassword, {});
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.success).toEqual(false);
|
||||
expect(response.message).toEqual("Verification failed");
|
||||
expect(userVerificationService.verifyUserByMasterPassword).toHaveBeenCalledWith(
|
||||
{
|
||||
type: VerificationType.MasterPassword,
|
||||
secret: mockMasterPassword,
|
||||
},
|
||||
activeAccount.id,
|
||||
activeAccount.email,
|
||||
);
|
||||
expect(masterPasswordService.decryptUserKeyWithMasterKey).not.toHaveBeenCalled();
|
||||
expect(keyService.setUserKey).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("calls convertToKeyConnectorCommand if required", () => {
|
||||
let convertToKeyConnectorSpy: jest.SpyInstance;
|
||||
beforeEach(() => {
|
||||
keyConnectorService.convertAccountRequired$ = of(true);
|
||||
|
||||
// Feature flag on
|
||||
masterPasswordUnlockService.unlockWithMasterPassword.mockResolvedValue(mockUserKey);
|
||||
|
||||
// Feature flag off
|
||||
userVerificationService.verifyUserByMasterPassword.mockResolvedValue({
|
||||
masterKey: mockMasterKey,
|
||||
} as MasterPasswordVerificationResponse);
|
||||
masterPasswordService.decryptUserKeyWithMasterKey.mockResolvedValue(mockUserKey);
|
||||
});
|
||||
|
||||
test.each([true, false])("returns failure when feature flag is %s", async (flagValue) => {
|
||||
configService.getFeatureFlag$.mockReturnValue(of(flagValue));
|
||||
|
||||
// Mock the ConvertToKeyConnectorCommand
|
||||
const mockRun = jest.fn().mockResolvedValue({ success: false, message: "convert failed" });
|
||||
convertToKeyConnectorSpy = jest
|
||||
.spyOn(ConvertToKeyConnectorCommand.prototype, "run")
|
||||
.mockImplementation(mockRun);
|
||||
|
||||
const response = await command.run(mockMasterPassword, {});
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.success).toEqual(false);
|
||||
expect(response.message).toEqual("convert failed");
|
||||
expect(keyService.setUserKey).toHaveBeenCalledWith(mockUserKey, activeAccount.id);
|
||||
expect(convertToKeyConnectorSpy).toHaveBeenCalled();
|
||||
|
||||
if (flagValue === true) {
|
||||
expect(masterPasswordUnlockService.unlockWithMasterPassword).toHaveBeenCalledWith(
|
||||
mockMasterPassword,
|
||||
activeAccount.id,
|
||||
);
|
||||
} else {
|
||||
expect(userVerificationService.verifyUserByMasterPassword).toHaveBeenCalledWith(
|
||||
{
|
||||
type: VerificationType.MasterPassword,
|
||||
secret: mockMasterPassword,
|
||||
},
|
||||
activeAccount.id,
|
||||
activeAccount.email,
|
||||
);
|
||||
expect(masterPasswordService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(
|
||||
mockMasterKey,
|
||||
activeAccount.id,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test.each([true, false])(
|
||||
"returns expected success when feature flag is %s",
|
||||
async (flagValue) => {
|
||||
configService.getFeatureFlag$.mockReturnValue(of(flagValue));
|
||||
|
||||
// Mock the ConvertToKeyConnectorCommand
|
||||
const mockRun = jest.fn().mockResolvedValue({ success: true });
|
||||
const convertToKeyConnectorSpy = jest
|
||||
.spyOn(ConvertToKeyConnectorCommand.prototype, "run")
|
||||
.mockImplementation(mockRun);
|
||||
|
||||
const response = await command.run(mockMasterPassword, {});
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.success).toEqual(true);
|
||||
expect(response.data).toEqual(expectedSuccessMessage);
|
||||
expect(keyService.setUserKey).toHaveBeenCalledWith(mockUserKey, activeAccount.id);
|
||||
expect(convertToKeyConnectorSpy).toHaveBeenCalled();
|
||||
|
||||
if (flagValue === true) {
|
||||
expect(masterPasswordUnlockService.unlockWithMasterPassword).toHaveBeenCalledWith(
|
||||
mockMasterPassword,
|
||||
activeAccount.id,
|
||||
);
|
||||
} else {
|
||||
expect(userVerificationService.verifyUserByMasterPassword).toHaveBeenCalledWith(
|
||||
{
|
||||
type: VerificationType.MasterPassword,
|
||||
secret: mockMasterPassword,
|
||||
},
|
||||
activeAccount.id,
|
||||
activeAccount.email,
|
||||
);
|
||||
expect(masterPasswordService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(
|
||||
mockMasterKey,
|
||||
activeAccount.id,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,26 +1,29 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
||||
import { MasterPasswordVerification } from "@bitwarden/common/auth/types/verification";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { MasterPasswordUnlockService } from "@bitwarden/common/key-management/master-password/abstractions/master-password-unlock.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
||||
import { MasterKey } from "@bitwarden/common/types/key";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { ConvertToKeyConnectorCommand } from "../../key-management/convert-to-key-connector.command";
|
||||
import { Response } from "../../models/response";
|
||||
import { MessageResponse } from "../../models/response/message.response";
|
||||
import { I18nService } from "../../platform/services/i18n.service";
|
||||
import { CliUtils } from "../../utils";
|
||||
import { ConvertToKeyConnectorCommand } from "../convert-to-key-connector.command";
|
||||
|
||||
export class UnlockCommand {
|
||||
constructor(
|
||||
@@ -35,6 +38,8 @@ export class UnlockCommand {
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private logout: () => Promise<void>,
|
||||
private i18nService: I18nService,
|
||||
private masterPasswordUnlockService: MasterPasswordUnlockService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async run(password: string, cmdOptions: Record<string, any>) {
|
||||
@@ -48,30 +53,53 @@ export class UnlockCommand {
|
||||
}
|
||||
|
||||
await this.setNewSessionKey();
|
||||
const [userId, email] = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => [a?.id, a?.email])),
|
||||
);
|
||||
|
||||
const verification = {
|
||||
type: VerificationType.MasterPassword,
|
||||
secret: password,
|
||||
} as MasterPasswordVerification;
|
||||
|
||||
let masterKey: MasterKey;
|
||||
try {
|
||||
const response = await this.userVerificationService.verifyUserByMasterPassword(
|
||||
verification,
|
||||
userId,
|
||||
email,
|
||||
);
|
||||
masterKey = response.masterKey;
|
||||
} catch (e) {
|
||||
// verification failure throws
|
||||
return Response.error(e.message);
|
||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
if (activeAccount == null) {
|
||||
return Response.error("No active account found");
|
||||
}
|
||||
const userId = activeAccount.id;
|
||||
|
||||
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey, userId);
|
||||
await this.keyService.setUserKey(userKey, userId);
|
||||
if (
|
||||
await firstValueFrom(
|
||||
this.configService.getFeatureFlag$(FeatureFlag.UnlockWithMasterPasswordUnlockData),
|
||||
)
|
||||
) {
|
||||
try {
|
||||
const userKey = await this.masterPasswordUnlockService.unlockWithMasterPassword(
|
||||
password,
|
||||
userId,
|
||||
);
|
||||
|
||||
await this.keyService.setUserKey(userKey, userId);
|
||||
} catch (e) {
|
||||
return Response.error(e.message);
|
||||
}
|
||||
} else {
|
||||
const email = activeAccount.email;
|
||||
const verification = {
|
||||
type: VerificationType.MasterPassword,
|
||||
secret: password,
|
||||
} as MasterPasswordVerification;
|
||||
|
||||
let masterKey: MasterKey;
|
||||
try {
|
||||
const response = await this.userVerificationService.verifyUserByMasterPassword(
|
||||
verification,
|
||||
userId,
|
||||
email,
|
||||
);
|
||||
masterKey = response.masterKey;
|
||||
} catch (e) {
|
||||
// verification failure throws
|
||||
return Response.error(e.message);
|
||||
}
|
||||
|
||||
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
|
||||
masterKey,
|
||||
userId,
|
||||
);
|
||||
await this.keyService.setUserKey(userKey, userId);
|
||||
}
|
||||
|
||||
if (await firstValueFrom(this.keyConnectorService.convertAccountRequired$)) {
|
||||
const convertToKeyConnectorCommand = new ConvertToKeyConnectorCommand(
|
||||
@@ -10,12 +10,12 @@ import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfirmCommand } from "./admin-console/commands/confirm.command";
|
||||
import { ShareCommand } from "./admin-console/commands/share.command";
|
||||
import { LockCommand } from "./auth/commands/lock.command";
|
||||
import { UnlockCommand } from "./auth/commands/unlock.command";
|
||||
import { EditCommand } from "./commands/edit.command";
|
||||
import { GetCommand } from "./commands/get.command";
|
||||
import { ListCommand } from "./commands/list.command";
|
||||
import { RestoreCommand } from "./commands/restore.command";
|
||||
import { StatusCommand } from "./commands/status.command";
|
||||
import { UnlockCommand } from "./key-management/commands/unlock.command";
|
||||
import { Response } from "./models/response";
|
||||
import { FileResponse } from "./models/response/file.response";
|
||||
import { ServiceContainer } from "./service-container/service-container";
|
||||
@@ -173,6 +173,8 @@ export class OssServeConfigurator {
|
||||
this.serviceContainer.organizationApiService,
|
||||
async () => await this.serviceContainer.logout(),
|
||||
this.serviceContainer.i18nService,
|
||||
this.serviceContainer.masterPasswordUnlockService,
|
||||
this.serviceContainer.configService,
|
||||
);
|
||||
|
||||
this.sendCreateCommand = new SendCreateCommand(
|
||||
@@ -211,6 +213,7 @@ export class OssServeConfigurator {
|
||||
this.serviceContainer.sendService,
|
||||
this.serviceContainer.sendApiService,
|
||||
this.serviceContainer.environmentService,
|
||||
this.serviceContainer.accountService,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,12 +10,12 @@ import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { LockCommand } from "./auth/commands/lock.command";
|
||||
import { LoginCommand } from "./auth/commands/login.command";
|
||||
import { LogoutCommand } from "./auth/commands/logout.command";
|
||||
import { UnlockCommand } from "./auth/commands/unlock.command";
|
||||
import { BaseProgram } from "./base-program";
|
||||
import { CompletionCommand } from "./commands/completion.command";
|
||||
import { EncodeCommand } from "./commands/encode.command";
|
||||
import { StatusCommand } from "./commands/status.command";
|
||||
import { UpdateCommand } from "./commands/update.command";
|
||||
import { UnlockCommand } from "./key-management/commands/unlock.command";
|
||||
import { Response } from "./models/response";
|
||||
import { MessageResponse } from "./models/response/message.response";
|
||||
import { ConfigCommand } from "./platform/commands/config.command";
|
||||
@@ -175,7 +175,7 @@ export class Program extends BaseProgram {
|
||||
const command = new LoginCommand(
|
||||
this.serviceContainer.loginStrategyService,
|
||||
this.serviceContainer.authService,
|
||||
this.serviceContainer.apiService,
|
||||
this.serviceContainer.twoFactorApiService,
|
||||
this.serviceContainer.masterPasswordApiService,
|
||||
this.serviceContainer.cryptoFunctionService,
|
||||
this.serviceContainer.environmentService,
|
||||
@@ -303,6 +303,8 @@ export class Program extends BaseProgram {
|
||||
this.serviceContainer.organizationApiService,
|
||||
async () => await this.serviceContainer.logout(),
|
||||
this.serviceContainer.i18nService,
|
||||
this.serviceContainer.masterPasswordUnlockService,
|
||||
this.serviceContainer.configService,
|
||||
);
|
||||
const response = await command.run(password, cmd);
|
||||
this.processResponse(response);
|
||||
|
||||
@@ -49,6 +49,7 @@ import { TokenService } from "@bitwarden/common/auth/services/token.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service";
|
||||
import { UserVerificationApiService } from "@bitwarden/common/auth/services/user-verification/user-verification-api.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service";
|
||||
import { TwoFactorApiService, DefaultTwoFactorApiService } from "@bitwarden/common/auth/two-factor";
|
||||
import {
|
||||
AutofillSettingsService,
|
||||
AutofillSettingsServiceAbstraction,
|
||||
@@ -69,10 +70,14 @@ import { EncryptServiceImplementation } from "@bitwarden/common/key-management/c
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { DeviceTrustService } from "@bitwarden/common/key-management/device-trust/services/device-trust.service.implementation";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/services/key-connector.service";
|
||||
import { MasterPasswordUnlockService } from "@bitwarden/common/key-management/master-password/abstractions/master-password-unlock.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { DefaultMasterPasswordUnlockService } from "@bitwarden/common/key-management/master-password/services/default-master-password-unlock.service";
|
||||
import { MasterPasswordService } from "@bitwarden/common/key-management/master-password/services/master-password.service";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { PinService } from "@bitwarden/common/key-management/pin/pin.service.implementation";
|
||||
import { SecurityStateService } from "@bitwarden/common/key-management/security-state/abstractions/security-state.service";
|
||||
import { DefaultSecurityStateService } from "@bitwarden/common/key-management/security-state/services/security-state.service";
|
||||
import {
|
||||
DefaultVaultTimeoutService,
|
||||
DefaultVaultTimeoutSettingsService,
|
||||
@@ -226,6 +231,7 @@ export class ServiceContainer {
|
||||
tokenService: TokenService;
|
||||
appIdService: AppIdService;
|
||||
apiService: NodeApiService;
|
||||
twoFactorApiService: TwoFactorApiService;
|
||||
hibpApiService: HibpApiService;
|
||||
environmentService: EnvironmentService;
|
||||
cipherService: CipherService;
|
||||
@@ -305,6 +311,8 @@ export class ServiceContainer {
|
||||
cipherEncryptionService: CipherEncryptionService;
|
||||
restrictedItemTypesService: RestrictedItemTypesService;
|
||||
cliRestrictedItemTypesService: CliRestrictedItemTypesService;
|
||||
securityStateService: SecurityStateService;
|
||||
masterPasswordUnlockService: MasterPasswordUnlockService;
|
||||
cipherArchiveService: CipherArchiveService;
|
||||
|
||||
constructor() {
|
||||
@@ -406,6 +414,8 @@ export class ServiceContainer {
|
||||
this.derivedStateProvider,
|
||||
);
|
||||
|
||||
this.securityStateService = new DefaultSecurityStateService(this.stateProvider);
|
||||
|
||||
this.environmentService = new DefaultEnvironmentService(
|
||||
this.stateProvider,
|
||||
this.accountService,
|
||||
@@ -473,6 +483,11 @@ export class ServiceContainer {
|
||||
this.kdfConfigService,
|
||||
);
|
||||
|
||||
this.masterPasswordUnlockService = new DefaultMasterPasswordUnlockService(
|
||||
this.masterPasswordService,
|
||||
this.keyService,
|
||||
);
|
||||
|
||||
this.appIdService = new AppIdService(this.storageService, this.logService);
|
||||
|
||||
const customUserAgent =
|
||||
@@ -523,6 +538,8 @@ export class ServiceContainer {
|
||||
|
||||
this.configApiService = new ConfigApiService(this.apiService);
|
||||
|
||||
this.twoFactorApiService = new DefaultTwoFactorApiService(this.apiService);
|
||||
|
||||
this.authService = new AuthService(
|
||||
this.accountService,
|
||||
this.messagingService,
|
||||
@@ -547,6 +564,7 @@ export class ServiceContainer {
|
||||
this.sendStateProvider = new SendStateProvider(this.stateProvider);
|
||||
|
||||
this.sendService = new SendService(
|
||||
this.accountService,
|
||||
this.keyService,
|
||||
this.i18nService,
|
||||
this.keyGenerationService,
|
||||
@@ -612,6 +630,7 @@ export class ServiceContainer {
|
||||
this.accountService,
|
||||
this.kdfConfigService,
|
||||
this.keyService,
|
||||
this.securityStateService,
|
||||
this.apiService,
|
||||
this.stateProvider,
|
||||
this.configService,
|
||||
@@ -818,6 +837,7 @@ export class ServiceContainer {
|
||||
this.tokenService,
|
||||
this.authService,
|
||||
this.stateProvider,
|
||||
this.securityStateService,
|
||||
);
|
||||
|
||||
this.totpService = new TotpService(this.sdkService);
|
||||
|
||||
@@ -6,6 +6,7 @@ import * as path from "path";
|
||||
import { firstValueFrom, switchMap } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
@@ -142,7 +143,8 @@ export class SendCreateCommand {
|
||||
|
||||
await this.sendApiService.save([encSend, fileData]);
|
||||
const newSend = await this.sendService.getFromState(encSend.id);
|
||||
const decSend = await newSend.decrypt();
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const decSend = await newSend.decrypt(activeUserId);
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const res = new SendResponse(decSend, env.getWebVaultUrl());
|
||||
return Response.success(res);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
@@ -83,7 +84,8 @@ export class SendEditCommand {
|
||||
return Response.error("Premium status is required to use this feature.");
|
||||
}
|
||||
|
||||
let sendView = await send.decrypt();
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
let sendView = await send.decrypt(activeUserId);
|
||||
sendView = SendResponse.toView(req, sendView);
|
||||
|
||||
try {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
|
||||
import { isGuid } from "@bitwarden/guid";
|
||||
|
||||
import { DownloadCommand } from "../../../commands/download.command";
|
||||
import { Response } from "../../../models/response";
|
||||
@@ -74,13 +75,13 @@ export class SendGetCommand extends DownloadCommand {
|
||||
}
|
||||
|
||||
private async getSendView(id: string): Promise<SendView | SendView[]> {
|
||||
if (Utils.isGuid(id)) {
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
if (isGuid(id)) {
|
||||
const send = await this.sendService.getFromState(id);
|
||||
if (send != null) {
|
||||
return await send.decrypt();
|
||||
return await send.decrypt(activeUserId);
|
||||
}
|
||||
} else if (id.trim() !== "") {
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
let sends = await this.sendService.getAllDecryptedFromState(activeUserId);
|
||||
sends = this.searchService.searchSends(sends, id);
|
||||
if (sends.length > 1) {
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// @ts-strict-ignore
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services//send.service.abstraction";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
@@ -14,6 +16,7 @@ export class SendRemovePasswordCommand {
|
||||
private sendService: SendService,
|
||||
private sendApiService: SendApiService,
|
||||
private environmentService: EnvironmentService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
async run(id: string) {
|
||||
@@ -21,7 +24,8 @@ export class SendRemovePasswordCommand {
|
||||
await this.sendApiService.removePassword(id);
|
||||
|
||||
const updatedSend = await firstValueFrom(this.sendService.get$(id));
|
||||
const decSend = await updatedSend.decrypt();
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const decSend = await updatedSend.decrypt(activeUserId);
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const webVaultUrl = env.getWebVaultUrl();
|
||||
const res = new SendResponse(decSend, webVaultUrl);
|
||||
|
||||
@@ -297,6 +297,7 @@ export class SendProgram extends BaseProgram {
|
||||
this.serviceContainer.sendService,
|
||||
this.serviceContainer.sendApiService,
|
||||
this.serviceContainer.environmentService,
|
||||
this.serviceContainer.accountService,
|
||||
);
|
||||
const response = await cmd.run(id);
|
||||
this.processResponse(response);
|
||||
|
||||
42
apps/desktop/desktop_native/Cargo.lock
generated
42
apps/desktop/desktop_native/Cargo.lock
generated
@@ -909,6 +909,22 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctor"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67773048316103656a637612c4a62477603b777d91d9c62ff2290f9cde178fdb"
|
||||
dependencies = [
|
||||
"ctor-proc-macro",
|
||||
"dtor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctor-proc-macro"
|
||||
version = "0.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2931af7e13dc045d8e9d26afccc6fa115d64e115c9c84b1166288b46f6782c2"
|
||||
|
||||
[[package]]
|
||||
name = "ctr"
|
||||
version = "0.9.2"
|
||||
@@ -1261,6 +1277,21 @@ version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
||||
|
||||
[[package]]
|
||||
name = "dtor"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e58a0764cddb55ab28955347b45be00ade43d4d6f3ba4bf3dc354e4ec9432934"
|
||||
dependencies = [
|
||||
"dtor-proc-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dtor-proc-macro"
|
||||
version = "0.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5"
|
||||
|
||||
[[package]]
|
||||
name = "ecdsa"
|
||||
version = "0.16.9"
|
||||
@@ -2164,7 +2195,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55740c4ae1d8696773c78fdafd5d0e5fe9bc9f1b071c7ba493ba5c413a9184f3"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"ctor",
|
||||
"ctor 0.2.9",
|
||||
"napi-derive",
|
||||
"napi-sys",
|
||||
"once_cell",
|
||||
@@ -2919,6 +2950,15 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "process_isolation"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"ctor 0.5.0",
|
||||
"desktop_core",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.37.5"
|
||||
|
||||
@@ -6,6 +6,7 @@ members = [
|
||||
"core",
|
||||
"macos_provider",
|
||||
"napi",
|
||||
"process_isolation",
|
||||
"proxy",
|
||||
"ssh_agent",
|
||||
"windows_plugin_authenticator",
|
||||
@@ -28,6 +29,7 @@ byteorder = "=1.5.0"
|
||||
bytes = "=1.10.1"
|
||||
cbc = "=0.1.2"
|
||||
core-foundation = "=0.10.1"
|
||||
ctor = "=0.5.0"
|
||||
dirs = "=6.0.0"
|
||||
ed25519 = "=2.2.3"
|
||||
embed_plist = "=1.2.2"
|
||||
|
||||
@@ -45,6 +45,20 @@ function buildProxyBin(target, release = true) {
|
||||
}
|
||||
}
|
||||
|
||||
function buildProcessIsolation() {
|
||||
if (process.platform !== "linux") {
|
||||
return;
|
||||
}
|
||||
|
||||
child_process.execSync(`cargo build --release`, {
|
||||
stdio: 'inherit',
|
||||
cwd: path.join(__dirname, "process_isolation")
|
||||
});
|
||||
|
||||
console.log("Copying process isolation library to dist folder");
|
||||
fs.copyFileSync(path.join(__dirname, "target", "release", "libprocess_isolation.so"), path.join(__dirname, "dist", `libprocess_isolation.so`));
|
||||
}
|
||||
|
||||
function installTarget(target) {
|
||||
child_process.execSync(`rustup target add ${target}`, { stdio: 'inherit', cwd: __dirname });
|
||||
}
|
||||
@@ -53,6 +67,7 @@ if (!crossPlatform && !target) {
|
||||
console.log(`Building native modules in ${mode} mode for the native architecture`);
|
||||
buildNapiModule(false, mode === "release");
|
||||
buildProxyBin(false, mode === "release");
|
||||
buildProcessIsolation();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -61,6 +76,7 @@ if (target) {
|
||||
installTarget(target);
|
||||
buildNapiModule(target, mode === "release");
|
||||
buildProxyBin(target, mode === "release");
|
||||
buildProcessIsolation();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -78,4 +94,5 @@ platformTargets.forEach(([target, _]) => {
|
||||
installTarget(target);
|
||||
buildNapiModule(target);
|
||||
buildProxyBin(target);
|
||||
buildProcessIsolation();
|
||||
});
|
||||
|
||||
14
apps/desktop/desktop_native/process_isolation/Cargo.toml
Normal file
14
apps/desktop/desktop_native/process_isolation/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "process_isolation"
|
||||
edition = { workspace = true }
|
||||
license = { workspace = true }
|
||||
version = { workspace = true }
|
||||
publish = { workspace = true }
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
ctor = { workspace = true }
|
||||
desktop_core = { path = "../core" }
|
||||
libc = { workspace = true }
|
||||
46
apps/desktop/desktop_native/process_isolation/src/lib.rs
Normal file
46
apps/desktop/desktop_native/process_isolation/src/lib.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
#![cfg(target_os = "linux")]
|
||||
|
||||
//! This library compiles to a pre-loadable shared object. When preloaded, it
|
||||
//! immediately isolates the process using the methods available on the platform.
|
||||
//! On Linux, this is PR_SET_DUMPABLE to prevent debuggers from attaching, the env
|
||||
//! from being read and the memory from being stolen.
|
||||
|
||||
use desktop_core::process_isolation;
|
||||
use std::{ffi::c_char, sync::LazyLock};
|
||||
|
||||
static ORIGINAL_UNSETENV: LazyLock<unsafe extern "C" fn(*const c_char) -> i32> =
|
||||
LazyLock::new(|| unsafe {
|
||||
std::mem::transmute(libc::dlsym(libc::RTLD_NEXT, c"unsetenv".as_ptr()))
|
||||
});
|
||||
|
||||
/// Hooks unsetenv to fix a bug in zypak-wrapper.
|
||||
/// Zypak unsets the env in Flatpak as a side-effect, which means that only the top level
|
||||
/// processes would be hooked. With this work-around all processes in the tree are hooked
|
||||
#[unsafe(no_mangle)]
|
||||
unsafe extern "C" fn unsetenv(name: *const c_char) -> i32 {
|
||||
unsafe {
|
||||
let Ok(name_str) = std::ffi::CStr::from_ptr(name).to_str() else {
|
||||
return ORIGINAL_UNSETENV(name);
|
||||
};
|
||||
|
||||
if name_str == "LD_PRELOAD" {
|
||||
// This env variable is provided by the flatpak configuration
|
||||
let ld_preload = std::env::var("PROCESS_ISOLATION_LD_PRELOAD").unwrap_or_default();
|
||||
std::env::set_var("LD_PRELOAD", ld_preload);
|
||||
return 0;
|
||||
}
|
||||
|
||||
ORIGINAL_UNSETENV(name)
|
||||
}
|
||||
}
|
||||
|
||||
// Hooks the shared object being loaded into the process
|
||||
#[ctor::ctor]
|
||||
fn preload_init() {
|
||||
let pid = unsafe { libc::getpid() };
|
||||
unsafe {
|
||||
println!("[Process Isolation] Enabling memory security for process {pid}");
|
||||
process_isolation::isolate_process();
|
||||
process_isolation::disable_coredumps();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script tests the memory isolation status of bitwarden-desktop processes. The script will print "isolated"
|
||||
# if the memory is not accessible by other processes.
|
||||
|
||||
CURRENT_USER=$(whoami)
|
||||
|
||||
# Find processes with "bitwarden" in the command
|
||||
pids=$(pgrep -f bitwarden)
|
||||
|
||||
if [[ -z "$pids" ]]; then
|
||||
echo "No bitwarden processes found."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for pid in $pids; do
|
||||
# Get process info: command, PPID, RSS memory
|
||||
read cmd ppid rss <<<$(ps -o comm=,ppid=,rss= -p "$pid")
|
||||
|
||||
# Explicitly skip if the command line does not contain "bitwarden"
|
||||
if ! grep -q "bitwarden" <<<"$cmd"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Check ownership of /proc/$pid/environ
|
||||
owner=$(stat -c "%U" /proc/$pid/environ 2>/dev/null)
|
||||
|
||||
if [[ "$owner" == "root" ]]; then
|
||||
status="isolated"
|
||||
elif [[ "$owner" == "$CURRENT_USER" ]]; then
|
||||
status="insecure"
|
||||
else
|
||||
status="unknown-owner:$owner"
|
||||
fi
|
||||
|
||||
# Convert memory to MB
|
||||
mem_mb=$((rss / 1024))
|
||||
|
||||
echo "PID: $pid | CMD: $cmd | Mem: ${mem_mb}MB | Owner: $owner | Status: $status"
|
||||
done
|
||||
132
apps/desktop/electron-builder.beta.json
Normal file
132
apps/desktop/electron-builder.beta.json
Normal file
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"extraMetadata": {
|
||||
"name": "bitwarden-beta"
|
||||
},
|
||||
"productName": "Bitwarden Beta",
|
||||
"appId": "com.bitwarden.desktop.beta",
|
||||
"buildDependenciesFromSource": true,
|
||||
"copyright": "Copyright © 2015-2025 Bitwarden Inc.",
|
||||
"directories": {
|
||||
"buildResources": "resources",
|
||||
"output": "dist",
|
||||
"app": "build"
|
||||
},
|
||||
"afterSign": "scripts/after-sign.js",
|
||||
"afterPack": "scripts/after-pack.js",
|
||||
"asarUnpack": ["**/*.node"],
|
||||
"files": [
|
||||
"**/*",
|
||||
"!**/node_modules/@bitwarden/desktop-napi/**/*",
|
||||
"**/node_modules/@bitwarden/desktop-napi/index.js",
|
||||
"**/node_modules/@bitwarden/desktop-napi/desktop_napi.${platform}-${arch}*.node"
|
||||
],
|
||||
"electronVersion": "36.8.1",
|
||||
"generateUpdatesFilesForAllChannels": true,
|
||||
"publish": {
|
||||
"provider": "generic",
|
||||
"url": "https://artifacts.bitwarden.com/desktop"
|
||||
},
|
||||
"win": {
|
||||
"electronUpdaterCompatibility": ">=0.0.1",
|
||||
"target": ["portable", "nsis-web", "appx"],
|
||||
"signtoolOptions": {
|
||||
"sign": "./sign.js"
|
||||
},
|
||||
"extraFiles": [
|
||||
{
|
||||
"from": "desktop_native/dist/desktop_proxy.${platform}-${arch}.exe",
|
||||
"to": "desktop_proxy.exe"
|
||||
}
|
||||
]
|
||||
},
|
||||
"nsisWeb": {
|
||||
"oneClick": false,
|
||||
"perMachine": false,
|
||||
"allowToChangeInstallationDirectory": false,
|
||||
"artifactName": "Bitwarden-Beta-Installer-${version}.${ext}",
|
||||
"uninstallDisplayName": "${productName}",
|
||||
"deleteAppDataOnUninstall": true,
|
||||
"include": "installer.nsh"
|
||||
},
|
||||
"portable": {
|
||||
"artifactName": "Bitwarden-Beta-Portable-${version}.${ext}"
|
||||
},
|
||||
"appx": {
|
||||
"artifactName": "Bitwarden-Beta-${version}-${arch}.${ext}",
|
||||
"backgroundColor": "#175DDC",
|
||||
"applicationId": "BitwardenBeta",
|
||||
"identityName": "8bitSolutionsLLC.BitwardenBeta",
|
||||
"publisher": "CN=14D52771-DE3C-4886-B8BF-825BA7690418",
|
||||
"publisherDisplayName": "Bitwarden Inc",
|
||||
"languages": [
|
||||
"en-US",
|
||||
"af",
|
||||
"ar",
|
||||
"az-latn",
|
||||
"be",
|
||||
"bg",
|
||||
"bn",
|
||||
"bs",
|
||||
"ca",
|
||||
"cs",
|
||||
"cy",
|
||||
"da",
|
||||
"de",
|
||||
"el",
|
||||
"en-gb",
|
||||
"en-in",
|
||||
"es",
|
||||
"et",
|
||||
"eu",
|
||||
"fa",
|
||||
"fi",
|
||||
"fil",
|
||||
"fr",
|
||||
"gl",
|
||||
"he",
|
||||
"hi",
|
||||
"hr",
|
||||
"hu",
|
||||
"id",
|
||||
"it",
|
||||
"ja",
|
||||
"ka",
|
||||
"km",
|
||||
"kn",
|
||||
"ko",
|
||||
"lt",
|
||||
"lv",
|
||||
"ml",
|
||||
"mr",
|
||||
"nb",
|
||||
"ne",
|
||||
"nl",
|
||||
"nn",
|
||||
"or",
|
||||
"pl",
|
||||
"pt-br",
|
||||
"pt-pt",
|
||||
"ro",
|
||||
"ru",
|
||||
"si",
|
||||
"sk",
|
||||
"sl",
|
||||
"sr-cyrl",
|
||||
"sv",
|
||||
"ta",
|
||||
"te",
|
||||
"th",
|
||||
"tr",
|
||||
"uk",
|
||||
"vi",
|
||||
"zh-cn",
|
||||
"zh-tw"
|
||||
]
|
||||
},
|
||||
"protocols": [
|
||||
{
|
||||
"name": "Bitwarden",
|
||||
"schemes": ["bitwarden"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -106,6 +106,10 @@
|
||||
{
|
||||
"from": "desktop_native/dist/desktop_proxy.${platform}-${arch}",
|
||||
"to": "desktop_proxy"
|
||||
},
|
||||
{
|
||||
"from": "desktop_native/dist/libprocess_isolation.so",
|
||||
"to": "libprocess_isolation.so"
|
||||
}
|
||||
],
|
||||
"target": ["deb", "freebsd", "rpm", "AppImage", "snap"],
|
||||
|
||||
@@ -35,9 +35,10 @@
|
||||
"build:renderer:watch": "cross-env NODE_ENV=development webpack --config webpack.config.js --config-name renderer --watch",
|
||||
"electron": "node ./scripts/start.js",
|
||||
"electron:ignore": "node ./scripts/start.js --ignore-certificate-errors",
|
||||
"flatpak:dev": "npm run clean:dist && electron-builder --dir -p never && flatpak-builder --force-clean --install --user ../../.flatpak/ ./resources/com.bitwarden.desktop.devel.yaml && flatpak run com.bitwarden.desktop",
|
||||
"clean:dist": "rimraf ./dist",
|
||||
"pack:dir": "npm run clean:dist && electron-builder --dir -p never",
|
||||
"pack:lin:flatpak": "npm run clean:dist && electron-builder --dir -p never && flatpak-builder --repo=build/.repo build/.flatpak ./resources/com.bitwarden.desktop.devel.yaml --install-deps-from=flathub --force-clean && flatpak build-bundle ./build/.repo/ ./dist/com.bitwarden.desktop.flatpak com.bitwarden.desktop",
|
||||
"pack:lin:flatpak": "flatpak-builder --repo=../../.flatpak-repo ../../.flatpak ./resources/com.bitwarden.desktop.devel.yaml --install-deps-from=flathub --force-clean && flatpak build-bundle ../../.flatpak-repo/ ./dist/com.bitwarden.desktop.flatpak com.bitwarden.desktop",
|
||||
"pack:lin": "npm run clean:dist && electron-builder --linux --x64 -p never && export SNAP_FILE=$(realpath ./dist/bitwarden_*.snap) && unsquashfs -d ./dist/tmp-snap/ $SNAP_FILE && mkdir -p ./dist/tmp-snap/meta/polkit/ && cp ./resources/com.bitwarden.desktop.policy ./dist/tmp-snap/meta/polkit/polkit.com.bitwarden.desktop.policy && rm $SNAP_FILE && snap pack --compression=lzo ./dist/tmp-snap/ && mv ./*.snap ./dist/ && rm -rf ./dist/tmp-snap/",
|
||||
"pack:lin:arm64": "npm run clean:dist && electron-builder --dir -p never && tar -czvf ./dist/bitwarden_desktop_arm64.tar.gz -C ./dist/linux-arm64-unpacked/ .",
|
||||
"pack:mac": "npm run clean:dist && electron-builder --mac --universal -p never",
|
||||
@@ -48,6 +49,7 @@
|
||||
"pack:mac:masdev": "npm run clean:dist && electron-builder --mac mas-dev --universal -p never",
|
||||
"pack:mac:masdev:with-extension": "npm run clean:dist && npm run build:macos-extension:masdev && electron-builder --mac mas-dev --universal -p never",
|
||||
"pack:win": "npm run clean:dist && electron-builder --win --x64 --arm64 --ia32 -p never -c.win.signtoolOptions.certificateSubjectName=\"8bit Solutions LLC\"",
|
||||
"pack:win:beta": "npm run clean:dist && electron-builder --config electron-builder.beta.json --win --x64 --arm64 --ia32 -p never -c.win.signtoolOptions.certificateSubjectName=\"8bit Solutions LLC\"",
|
||||
"pack:win:ci": "npm run clean:dist && electron-builder --win --x64 --arm64 --ia32 -p never",
|
||||
"dist:dir": "npm run build && npm run pack:dir",
|
||||
"dist:lin": "npm run build && npm run pack:lin",
|
||||
|
||||
115
apps/desktop/project.json
Normal file
115
apps/desktop/project.json
Normal file
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"name": "desktop",
|
||||
"projectType": "application",
|
||||
"sourceRoot": "apps/desktop/src",
|
||||
"tags": ["scope:desktop", "type:app"],
|
||||
"targets": {
|
||||
"build-native": {
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"command": "cd desktop_native && node build.js",
|
||||
"cwd": "apps/desktop"
|
||||
}
|
||||
},
|
||||
"build-main": {
|
||||
"executor": "nx:run-commands",
|
||||
"outputs": ["{workspaceRoot}/dist/apps/desktop"],
|
||||
"options": {
|
||||
"command": "cross-env NODE_ENV=production webpack --config webpack.config.js --config-name main",
|
||||
"cwd": "apps/desktop"
|
||||
},
|
||||
"configurations": {
|
||||
"development": {
|
||||
"command": "cross-env NODE_ENV=development webpack --config webpack.config.js --config-name main"
|
||||
},
|
||||
"production": {
|
||||
"command": "cross-env NODE_ENV=production webpack --config webpack.config.js --config-name main"
|
||||
}
|
||||
}
|
||||
},
|
||||
"build-preload": {
|
||||
"executor": "nx:run-commands",
|
||||
"outputs": ["{workspaceRoot}/dist/apps/desktop"],
|
||||
"options": {
|
||||
"command": "cross-env NODE_ENV=production webpack --config webpack.config.js --config-name preload",
|
||||
"cwd": "apps/desktop"
|
||||
},
|
||||
"configurations": {
|
||||
"development": {
|
||||
"command": "cross-env NODE_ENV=development webpack --config webpack.config.js --config-name preload"
|
||||
},
|
||||
"production": {
|
||||
"command": "cross-env NODE_ENV=production webpack --config webpack.config.js --config-name preload"
|
||||
}
|
||||
}
|
||||
},
|
||||
"build-renderer": {
|
||||
"executor": "nx:run-commands",
|
||||
"outputs": ["{workspaceRoot}/dist/apps/desktop"],
|
||||
"options": {
|
||||
"command": "cross-env NODE_ENV=production webpack --config webpack.config.js --config-name renderer",
|
||||
"cwd": "apps/desktop"
|
||||
},
|
||||
"configurations": {
|
||||
"development": {
|
||||
"command": "cross-env NODE_ENV=development webpack --config webpack.config.js --config-name renderer"
|
||||
},
|
||||
"production": {
|
||||
"command": "cross-env NODE_ENV=production webpack --config webpack.config.js --config-name renderer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"build": {
|
||||
"executor": "nx:run-commands",
|
||||
"dependsOn": ["build-native"],
|
||||
"outputs": ["{workspaceRoot}/dist/apps/desktop"],
|
||||
"options": {
|
||||
"parallel": true,
|
||||
"commands": [
|
||||
"nx run desktop:build-main",
|
||||
"nx run desktop:build-preload",
|
||||
"nx run desktop:build-renderer"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"development": {
|
||||
"commands": [
|
||||
"nx run desktop:build-main --configuration=development",
|
||||
"nx run desktop:build-preload --configuration=development",
|
||||
"nx run desktop:build-renderer --configuration=development"
|
||||
]
|
||||
},
|
||||
"production": {
|
||||
"commands": [
|
||||
"nx run desktop:build-main --configuration=production",
|
||||
"nx run desktop:build-preload --configuration=production",
|
||||
"nx run desktop:build-renderer --configuration=production"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"executor": "nx:run-commands",
|
||||
"dependsOn": ["build-native"],
|
||||
"options": {
|
||||
"command": "node scripts/nx-serve.js",
|
||||
"cwd": "apps/desktop"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/apps/desktop"],
|
||||
"options": {
|
||||
"jestConfig": "apps/desktop/jest.config.js"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint",
|
||||
"outputs": ["{options.outputFile}"],
|
||||
"options": {
|
||||
"lintFilePatterns": ["apps/desktop/**/*.ts", "apps/desktop/**/*.html"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,4 +46,6 @@ modules:
|
||||
commands:
|
||||
- ulimit -c 0
|
||||
- export TMPDIR="$XDG_RUNTIME_DIR/app/$FLATPAK_ID"
|
||||
- export ZYPAK_LD_PRELOAD="/app/bin/libprocess_isolation.so"
|
||||
- export PROCESS_ISOLATION_LD_PRELOAD="/app/bin/libprocess_isolation.so"
|
||||
- exec zypak-wrapper /app/bin/bitwarden-app "$@"
|
||||
|
||||
@@ -7,12 +7,19 @@ ulimit -c 0
|
||||
RAW_PATH=$(readlink -f "$0")
|
||||
APP_PATH=$(dirname $RAW_PATH)
|
||||
|
||||
# force use of base image libdus in snap
|
||||
if [ -e "/usr/lib/x86_64-linux-gnu/libdbus-1.so.3" ]
|
||||
then
|
||||
# force use of base image libdbus in snap
|
||||
if [ -e "/usr/lib/x86_64-linux-gnu/libdbus-1.so.3" ]; then
|
||||
export LD_PRELOAD="/usr/lib/x86_64-linux-gnu/libdbus-1.so.3"
|
||||
fi
|
||||
|
||||
# If running in non-snap, add libprocess_isolation.so from app path to LD_PRELOAD
|
||||
# This prevents debugger / memory dumping on all desktop processes
|
||||
if [ -z "$SNAP" ] && [ -f "$APP_PATH/libprocess_isolation.so" ]; then
|
||||
LIBPROCESS_ISOLATION_SO="$APP_PATH/libprocess_isolation.so"
|
||||
LD_PRELOAD="$LIBPROCESS_ISOLATION_SO${LD_PRELOAD:+:$LD_PRELOAD}"
|
||||
export LD_PRELOAD
|
||||
fi
|
||||
|
||||
PARAMS="--enable-features=UseOzonePlatform,WaylandWindowDecorations --ozone-platform-hint=auto"
|
||||
if [ "$USE_X11" = "true" ]; then
|
||||
PARAMS=""
|
||||
|
||||
42
apps/desktop/scripts/nx-serve.js
Normal file
42
apps/desktop/scripts/nx-serve.js
Normal file
@@ -0,0 +1,42 @@
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const path = require("path");
|
||||
|
||||
const concurrently = require("concurrently");
|
||||
const rimraf = require("rimraf");
|
||||
const args = process.argv.splice(2);
|
||||
const outputPath = path.resolve(__dirname, "../../../dist/apps/desktop");
|
||||
|
||||
rimraf.sync(outputPath);
|
||||
require("fs").mkdirSync(outputPath, { recursive: true });
|
||||
|
||||
concurrently(
|
||||
[
|
||||
{
|
||||
name: "Main",
|
||||
command: `cross-env NODE_ENV=development OUTPUT_PATH=${outputPath} webpack --config webpack.config.js --config-name main --watch`,
|
||||
prefixColor: "yellow",
|
||||
},
|
||||
{
|
||||
name: "Prel",
|
||||
command: `cross-env NODE_ENV=development OUTPUT_PATH=${outputPath} webpack --config webpack.config.js --config-name preload --watch`,
|
||||
prefixColor: "magenta",
|
||||
},
|
||||
{
|
||||
name: "Rend",
|
||||
command: `cross-env NODE_ENV=development OUTPUT_PATH=${outputPath} webpack --config webpack.config.js --config-name renderer --watch`,
|
||||
prefixColor: "cyan",
|
||||
},
|
||||
{
|
||||
name: "Elec",
|
||||
command: `npx wait-on ${outputPath}/main.js ${outputPath}/index.html && npx electron --no-sandbox --inspect=5858 ${args.join(
|
||||
" ",
|
||||
)} ${outputPath} --watch`,
|
||||
prefixColor: "green",
|
||||
},
|
||||
],
|
||||
{
|
||||
prefix: "name",
|
||||
outputStream: process.stdout,
|
||||
killOthers: ["success", "failure"],
|
||||
},
|
||||
);
|
||||
@@ -13,7 +13,7 @@ exports.default = async function (configuration) {
|
||||
`-fd ${configuration.hash} ` +
|
||||
`-du ${configuration.site} ` +
|
||||
`-tr http://timestamp.digicert.com ` +
|
||||
`${configuration.path}`,
|
||||
`"${configuration.path}"`,
|
||||
{
|
||||
stdio: "inherit",
|
||||
},
|
||||
|
||||
@@ -629,7 +629,6 @@ describe("SettingsComponent", () => {
|
||||
});
|
||||
|
||||
it("should not save vault timeout when vault timeout is invalid", async () => {
|
||||
i18nService.t.mockReturnValue("Number too large test error");
|
||||
component["form"].controls.vaultTimeout.setErrors({}, { emitEvent: false });
|
||||
await component.saveVaultTimeout(DEFAULT_VAULT_TIMEOUT, 999_999_999);
|
||||
|
||||
@@ -639,11 +638,6 @@ describe("SettingsComponent", () => {
|
||||
DEFAULT_VAULT_TIMEOUT_ACTION,
|
||||
);
|
||||
expect(component["form"].getRawValue().vaultTimeout).toEqual(DEFAULT_VAULT_TIMEOUT);
|
||||
expect(platformUtilsService.showToast).toHaveBeenCalledWith(
|
||||
"error",
|
||||
null,
|
||||
"Number too large test error",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -510,16 +510,11 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
// Avoid saving 0 since it's useless as a timeout value.
|
||||
if (this.form.value.vaultTimeout === 0) {
|
||||
if (newValue === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.form.controls.vaultTimeout.valid) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
null,
|
||||
this.i18nService.t("vaultTimeoutTooLarge"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
import { CommonModule, DatePipe } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/tools/send/add-edit.component";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -63,7 +65,8 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||
|
||||
async refresh() {
|
||||
const send = await this.loadSend();
|
||||
this.send = await send.decrypt();
|
||||
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
this.send = await send.decrypt(userId);
|
||||
this.updateFormValues();
|
||||
this.hasPassword = this.send.password != null && this.send.password.trim() !== "";
|
||||
}
|
||||
|
||||
@@ -2549,6 +2549,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"vaultCustomTimeoutMinimum": {
|
||||
"message": "Minimum custom timeout is 1 minute."
|
||||
},
|
||||
"inviteAccepted": {
|
||||
"message": "Invitation accepted"
|
||||
},
|
||||
@@ -4149,7 +4152,7 @@
|
||||
"message": "Archive",
|
||||
"description": "Verb"
|
||||
},
|
||||
"unarchive": {
|
||||
"unArchive": {
|
||||
"message": "Unarchive"
|
||||
},
|
||||
"itemsInArchive": {
|
||||
@@ -4161,11 +4164,11 @@
|
||||
"noItemsInArchiveDesc": {
|
||||
"message": "Archived items will appear here and will be excluded from general search results and autofill suggestions."
|
||||
},
|
||||
"itemSentToArchive": {
|
||||
"message": "Item sent to archive"
|
||||
"itemWasSentToArchive": {
|
||||
"message": "Item was sent to archive"
|
||||
},
|
||||
"itemRemovedFromArchive": {
|
||||
"message": "Item removed from archive"
|
||||
"itemUnarchived": {
|
||||
"message": "Item was unarchived"
|
||||
},
|
||||
"archiveItem": {
|
||||
"message": "Archive item"
|
||||
|
||||
@@ -12,10 +12,13 @@ import { MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
|
||||
/**
|
||||
* The SSO Localhost login service uses a local host listener as fallback in case scheme handling deeplinks does not work.
|
||||
* This way it is possible to log in with SSO on appimage, snap, and electron dev using the same methods that the cli uses.
|
||||
* This way it is possible to log in with SSO on appimage and electron dev using the same methods that the cli uses.
|
||||
*/
|
||||
export class SSOLocalhostCallbackService {
|
||||
private ssoRedirectUri = "";
|
||||
// We will only track one server at a time for use-case and performance considerations.
|
||||
// This will result in a last-one-wins behavior if multiple SSO flows are started simultaneously.
|
||||
private currentServer: http.Server | null = null;
|
||||
|
||||
constructor(
|
||||
private environmentService: EnvironmentService,
|
||||
@@ -23,11 +26,30 @@ export class SSOLocalhostCallbackService {
|
||||
private ssoUrlService: SsoUrlService,
|
||||
) {
|
||||
ipcMain.handle("openSsoPrompt", async (event, { codeChallenge, state, email }) => {
|
||||
const { ssoCode, recvState } = await this.openSsoPrompt(codeChallenge, state, email);
|
||||
this.messagingService.send("ssoCallback", {
|
||||
code: ssoCode,
|
||||
state: recvState,
|
||||
redirectUri: this.ssoRedirectUri,
|
||||
// Close any existing server before starting new one
|
||||
if (this.currentServer) {
|
||||
await this.closeCurrentServer();
|
||||
}
|
||||
|
||||
return this.openSsoPrompt(codeChallenge, state, email).then(({ ssoCode, recvState }) => {
|
||||
this.messagingService.send("ssoCallback", {
|
||||
code: ssoCode,
|
||||
state: recvState,
|
||||
redirectUri: this.ssoRedirectUri,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async closeCurrentServer(): Promise<void> {
|
||||
if (!this.currentServer) {
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
this.currentServer!.close(() => {
|
||||
this.currentServer = null;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -59,6 +81,7 @@ export class SSOLocalhostCallbackService {
|
||||
"<p>You may now close this tab and return to the app.</p>" +
|
||||
"</body></html>",
|
||||
);
|
||||
this.currentServer = null;
|
||||
callbackServer.close(() =>
|
||||
resolve({
|
||||
ssoCode: code,
|
||||
@@ -73,41 +96,68 @@ export class SSOLocalhostCallbackService {
|
||||
"<p>You may now close this tab and return to the app.</p>" +
|
||||
"</body></html>",
|
||||
);
|
||||
this.currentServer = null;
|
||||
callbackServer.close(() => reject());
|
||||
}
|
||||
});
|
||||
|
||||
let foundPort = false;
|
||||
const webUrl = env.getWebVaultUrl();
|
||||
for (let port = 8065; port <= 8070; port++) {
|
||||
try {
|
||||
this.ssoRedirectUri = "http://localhost:" + port;
|
||||
const ssoUrl = this.ssoUrlService.buildSsoUrl(
|
||||
webUrl,
|
||||
ClientType.Desktop,
|
||||
this.ssoRedirectUri,
|
||||
state,
|
||||
codeChallenge,
|
||||
email,
|
||||
);
|
||||
callbackServer.listen(port, () => {
|
||||
this.messagingService.send("launchUri", {
|
||||
url: ssoUrl,
|
||||
});
|
||||
});
|
||||
foundPort = true;
|
||||
break;
|
||||
} catch {
|
||||
// Ignore error since we run the same command up to 5 times.
|
||||
}
|
||||
}
|
||||
if (!foundPort) {
|
||||
reject();
|
||||
}
|
||||
// Store reference to current server
|
||||
this.currentServer = callbackServer;
|
||||
|
||||
// after 5 minutes, close the server
|
||||
const webUrl = env.getWebVaultUrl();
|
||||
|
||||
const tryNextPort = (port: number) => {
|
||||
if (port > 8070) {
|
||||
this.currentServer = null;
|
||||
reject("All available SSO ports in use");
|
||||
return;
|
||||
}
|
||||
|
||||
this.ssoRedirectUri = "http://localhost:" + port;
|
||||
const ssoUrl = this.ssoUrlService.buildSsoUrl(
|
||||
webUrl,
|
||||
ClientType.Desktop,
|
||||
this.ssoRedirectUri,
|
||||
state,
|
||||
codeChallenge,
|
||||
email,
|
||||
);
|
||||
|
||||
// Set up error handler before attempting to listen
|
||||
callbackServer.once("error", (err: any) => {
|
||||
if (err.code === "EADDRINUSE") {
|
||||
// Port is in use, try next port
|
||||
tryNextPort(port + 1);
|
||||
} else {
|
||||
// Another error - reject and set the current server to null
|
||||
// (one server alive at a time)
|
||||
this.currentServer = null;
|
||||
reject();
|
||||
}
|
||||
});
|
||||
|
||||
// Attempt to listen on the port
|
||||
callbackServer.listen(port, () => {
|
||||
// Success - remove error listener and launch SSO
|
||||
callbackServer.removeAllListeners("error");
|
||||
|
||||
this.messagingService.send("launchUri", {
|
||||
url: ssoUrl,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Start trying from port 8065
|
||||
tryNextPort(8065);
|
||||
|
||||
// Don't allow any server to stay up for more than 5 minutes;
|
||||
// this gives plenty of time to complete SSO but ensures we don't
|
||||
// have a server running indefinitely.
|
||||
setTimeout(
|
||||
() => {
|
||||
if (this.currentServer === callbackServer) {
|
||||
this.currentServer = null;
|
||||
}
|
||||
callbackServer.close(() => reject());
|
||||
},
|
||||
5 * 60 * 1000,
|
||||
|
||||
@@ -53,7 +53,8 @@ export function isWindowsStore() {
|
||||
if (
|
||||
windows &&
|
||||
!windowsStore &&
|
||||
process.resourcesPath?.indexOf("8bitSolutionsLLC.bitwardendesktop_") > -1
|
||||
(process.resourcesPath?.indexOf("8bitSolutionsLLC.bitwardendesktop_") > -1 ||
|
||||
process.resourcesPath?.indexOf("8bitSolutionsLLC.BitwardenBeta_") > -1)
|
||||
) {
|
||||
windowsStore = true;
|
||||
}
|
||||
|
||||
@@ -723,9 +723,7 @@ export class VaultV2Component<C extends CipherViewLike>
|
||||
|
||||
this.cipherId = cipher.id;
|
||||
this.cipher = cipher;
|
||||
if (this.activeUserId) {
|
||||
await this.cipherService.clearCache(this.activeUserId).catch(() => {});
|
||||
}
|
||||
|
||||
await this.vaultItemsComponent?.load(this.activeFilter.buildFilter()).catch(() => {});
|
||||
await this.go().catch(() => {});
|
||||
await this.vaultItemsComponent?.refresh().catch(() => {});
|
||||
|
||||
@@ -8,7 +8,7 @@ const { AngularWebpackPlugin } = require("@ngtools/webpack");
|
||||
const TerserPlugin = require("terser-webpack-plugin");
|
||||
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
|
||||
const { EnvironmentPlugin, DefinePlugin } = require("webpack");
|
||||
const configurator = require("./config/config");
|
||||
const configurator = require(path.resolve(__dirname, "config/config"));
|
||||
|
||||
module.exports.getEnv = function getEnv() {
|
||||
const NODE_ENV = process.env.NODE_ENV == null ? "development" : process.env.NODE_ENV;
|
||||
@@ -17,6 +17,14 @@ module.exports.getEnv = function getEnv() {
|
||||
return { NODE_ENV, ENV };
|
||||
};
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
outputPath: process.env.OUTPUT_PATH
|
||||
? path.isAbsolute(process.env.OUTPUT_PATH)
|
||||
? process.env.OUTPUT_PATH
|
||||
: path.resolve(__dirname, process.env.OUTPUT_PATH)
|
||||
: path.resolve(__dirname, "build"),
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* configName: string;
|
||||
@@ -33,9 +41,11 @@ module.exports.getEnv = function getEnv() {
|
||||
* entry: string;
|
||||
* tsConfig: string;
|
||||
* };
|
||||
* outputPath?: string;
|
||||
* }} params
|
||||
*/
|
||||
module.exports.buildConfig = function buildConfig(params) {
|
||||
params = { ...DEFAULT_PARAMS, ...params };
|
||||
const { NODE_ENV, ENV } = module.exports.getEnv();
|
||||
|
||||
console.log(`Building ${params.configName} Desktop App`);
|
||||
@@ -47,13 +57,16 @@ module.exports.buildConfig = function buildConfig(params) {
|
||||
resolve: {
|
||||
extensions: [".tsx", ".ts", ".js"],
|
||||
symlinks: false,
|
||||
modules: [path.resolve("../../node_modules")],
|
||||
modules: [
|
||||
path.resolve(__dirname, "../../node_modules"),
|
||||
path.resolve(process.cwd(), "node_modules"),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const getOutputConfig = (isDev) => ({
|
||||
filename: "[name].js",
|
||||
path: path.resolve(__dirname, "build"),
|
||||
path: params.outputPath,
|
||||
...(isDev && { devtoolModuleFilenameTemplate: "[absolute-resource-path]" }),
|
||||
});
|
||||
|
||||
@@ -96,9 +109,9 @@ module.exports.buildConfig = function buildConfig(params) {
|
||||
plugins: [
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
"./src/package.json",
|
||||
{ from: "./src/images", to: "images" },
|
||||
{ from: "./src/locales", to: "locales" },
|
||||
path.resolve(__dirname, "src/package.json"),
|
||||
{ from: path.resolve(__dirname, "src/images"), to: "images" },
|
||||
{ from: path.resolve(__dirname, "src/locales"), to: "locales" },
|
||||
],
|
||||
}),
|
||||
new DefinePlugin({
|
||||
@@ -164,7 +177,7 @@ module.exports.buildConfig = function buildConfig(params) {
|
||||
},
|
||||
output: {
|
||||
filename: "[name].js",
|
||||
path: path.resolve(__dirname, "build"),
|
||||
path: params.outputPath,
|
||||
},
|
||||
optimization: {
|
||||
minimizer: [
|
||||
@@ -200,7 +213,7 @@ module.exports.buildConfig = function buildConfig(params) {
|
||||
{
|
||||
loader: "babel-loader",
|
||||
options: {
|
||||
configFile: "../../babel.config.json",
|
||||
configFile: path.resolve(__dirname, "../../babel.config.json"),
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -293,7 +306,7 @@ module.exports.buildConfig = function buildConfig(params) {
|
||||
path.resolve(__dirname, "./src"),
|
||||
),
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/index.html",
|
||||
template: path.resolve(__dirname, "src/index.html"),
|
||||
filename: "index.html",
|
||||
chunks: ["app/vendor", "app/main"],
|
||||
}),
|
||||
|
||||
@@ -1,18 +1,43 @@
|
||||
const path = require("path");
|
||||
const { buildConfig } = require("./webpack.base");
|
||||
|
||||
module.exports = buildConfig({
|
||||
configName: "OSS",
|
||||
renderer: {
|
||||
entry: "./src/app/main.ts",
|
||||
entryModule: "src/app/app.module#AppModule",
|
||||
tsConfig: "./tsconfig.renderer.json",
|
||||
},
|
||||
main: {
|
||||
entry: "./src/entry.ts",
|
||||
tsConfig: "./tsconfig.json",
|
||||
},
|
||||
preload: {
|
||||
entry: "./src/preload.ts",
|
||||
tsConfig: "./tsconfig.json",
|
||||
},
|
||||
});
|
||||
module.exports = (webpackConfig, context) => {
|
||||
const isNxBuild = context && context.options;
|
||||
|
||||
if (isNxBuild) {
|
||||
return buildConfig({
|
||||
configName: "OSS",
|
||||
renderer: {
|
||||
entry: path.resolve(__dirname, "src/app/main.ts"),
|
||||
entryModule: "src/app/app.module#AppModule",
|
||||
tsConfig: path.resolve(context.context.root, "apps/desktop/tsconfig.renderer.json"),
|
||||
},
|
||||
main: {
|
||||
entry: path.resolve(__dirname, "src/entry.ts"),
|
||||
tsConfig: path.resolve(context.context.root, "apps/desktop/tsconfig.json"),
|
||||
},
|
||||
preload: {
|
||||
entry: path.resolve(__dirname, "src/preload.ts"),
|
||||
tsConfig: path.resolve(context.context.root, "apps/desktop/tsconfig.json"),
|
||||
},
|
||||
outputPath: path.resolve(context.context.root, context.options.outputPath),
|
||||
});
|
||||
} else {
|
||||
return buildConfig({
|
||||
configName: "OSS",
|
||||
renderer: {
|
||||
entry: path.resolve(__dirname, "src/app/main.ts"),
|
||||
entryModule: "src/app/app.module#AppModule",
|
||||
tsConfig: path.resolve(__dirname, "tsconfig.renderer.json"),
|
||||
},
|
||||
main: {
|
||||
entry: path.resolve(__dirname, "src/entry.ts"),
|
||||
tsConfig: path.resolve(__dirname, "tsconfig.json"),
|
||||
},
|
||||
preload: {
|
||||
entry: path.resolve(__dirname, "src/preload.ts"),
|
||||
tsConfig: path.resolve(__dirname, "tsconfig.json"),
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const path = require("path");
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require("postcss-import"),
|
||||
require("postcss-import")({
|
||||
path: [path.resolve(__dirname, "../../libs"), path.resolve(__dirname, "src/scss")],
|
||||
}),
|
||||
require("postcss-nested"),
|
||||
require("tailwindcss"),
|
||||
require("tailwindcss")({ config: path.resolve(__dirname, "tailwind.config.js") }),
|
||||
require("autoprefixer"),
|
||||
],
|
||||
};
|
||||
|
||||
215
apps/web/project.json
Normal file
215
apps/web/project.json
Normal file
@@ -0,0 +1,215 @@
|
||||
{
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"name": "web",
|
||||
"projectType": "application",
|
||||
"sourceRoot": "apps/web/src",
|
||||
"tags": ["scope:web", "type:app"],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nx/webpack:webpack",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"defaultConfiguration": "oss",
|
||||
"options": {
|
||||
"outputPath": "dist/apps/web",
|
||||
"webpackConfig": "apps/web/webpack.config.js",
|
||||
"tsConfig": "apps/web/tsconfig.json",
|
||||
"main": "apps/web/src/main.ts",
|
||||
"target": "web",
|
||||
"compiler": "tsc"
|
||||
},
|
||||
"configurations": {
|
||||
"oss": {
|
||||
"mode": "production",
|
||||
"outputPath": "dist/apps/web/oss"
|
||||
},
|
||||
"oss-dev": {
|
||||
"mode": "development",
|
||||
"outputPath": "dist/apps/web/oss-dev",
|
||||
"env": {
|
||||
"NODE_ENV": "development",
|
||||
"ENV": "development"
|
||||
}
|
||||
},
|
||||
"commercial": {
|
||||
"mode": "production",
|
||||
"outputPath": "dist/apps/web/commercial",
|
||||
"webpackConfig": "bitwarden_license/bit-web/webpack.config.js",
|
||||
"main": "bitwarden_license/bit-web/src/main.ts"
|
||||
},
|
||||
"commercial-dev": {
|
||||
"mode": "development",
|
||||
"outputPath": "dist/apps/web/commercial-dev",
|
||||
"webpackConfig": "bitwarden_license/bit-web/webpack.config.js",
|
||||
"main": "bitwarden_license/bit-web/src/main.ts",
|
||||
"env": {
|
||||
"NODE_ENV": "development",
|
||||
"ENV": "development"
|
||||
}
|
||||
},
|
||||
"commercial-qa": {
|
||||
"mode": "production",
|
||||
"outputPath": "dist/apps/web/commercial-qa",
|
||||
"webpackConfig": "bitwarden_license/bit-web/webpack.config.js",
|
||||
"main": "bitwarden_license/bit-web/src/main.ts",
|
||||
"env": {
|
||||
"NODE_ENV": "production",
|
||||
"ENV": "qa"
|
||||
}
|
||||
},
|
||||
"commercial-cloud": {
|
||||
"mode": "production",
|
||||
"outputPath": "dist/apps/web/commercial-cloud",
|
||||
"webpackConfig": "bitwarden_license/bit-web/webpack.config.js",
|
||||
"main": "bitwarden_license/bit-web/src/main.ts",
|
||||
"env": {
|
||||
"NODE_ENV": "production",
|
||||
"ENV": "cloud"
|
||||
}
|
||||
},
|
||||
"commercial-euprd": {
|
||||
"mode": "production",
|
||||
"outputPath": "dist/apps/web/commercial-euprd",
|
||||
"webpackConfig": "bitwarden_license/bit-web/webpack.config.js",
|
||||
"main": "bitwarden_license/bit-web/src/main.ts",
|
||||
"env": {
|
||||
"NODE_ENV": "production",
|
||||
"ENV": "euprd"
|
||||
}
|
||||
},
|
||||
"commercial-euqa": {
|
||||
"mode": "production",
|
||||
"outputPath": "dist/apps/web/commercial-euqa",
|
||||
"webpackConfig": "bitwarden_license/bit-web/webpack.config.js",
|
||||
"main": "bitwarden_license/bit-web/src/main.ts",
|
||||
"env": {
|
||||
"NODE_ENV": "production",
|
||||
"ENV": "euqa"
|
||||
}
|
||||
},
|
||||
"commercial-usdev": {
|
||||
"mode": "production",
|
||||
"outputPath": "dist/apps/web/commercial-usdev",
|
||||
"webpackConfig": "bitwarden_license/bit-web/webpack.config.js",
|
||||
"main": "bitwarden_license/bit-web/src/main.ts",
|
||||
"env": {
|
||||
"NODE_ENV": "production",
|
||||
"ENV": "usdev"
|
||||
}
|
||||
},
|
||||
"commercial-ee": {
|
||||
"mode": "production",
|
||||
"outputPath": "dist/apps/web/commercial-ee",
|
||||
"webpackConfig": "bitwarden_license/bit-web/webpack.config.js",
|
||||
"main": "bitwarden_license/bit-web/src/main.ts",
|
||||
"env": {
|
||||
"NODE_ENV": "production",
|
||||
"ENV": "ee"
|
||||
}
|
||||
},
|
||||
"oss-selfhost": {
|
||||
"mode": "production",
|
||||
"outputPath": "dist/apps/web/oss-selfhost",
|
||||
"env": {
|
||||
"ENV": "selfhosted",
|
||||
"NODE_ENV": "production"
|
||||
}
|
||||
},
|
||||
"oss-selfhost-dev": {
|
||||
"mode": "development",
|
||||
"outputPath": "dist/apps/web/oss-selfhost-dev",
|
||||
"env": {
|
||||
"NODE_ENV": "development",
|
||||
"ENV": "selfhosted"
|
||||
}
|
||||
},
|
||||
"commercial-selfhost": {
|
||||
"mode": "production",
|
||||
"outputPath": "dist/apps/web/commercial-selfhost",
|
||||
"webpackConfig": "bitwarden_license/bit-web/webpack.config.js",
|
||||
"main": "bitwarden_license/bit-web/src/main.ts",
|
||||
"env": {
|
||||
"ENV": "selfhosted",
|
||||
"NODE_ENV": "production"
|
||||
}
|
||||
},
|
||||
"commercial-selfhost-dev": {
|
||||
"mode": "development",
|
||||
"outputPath": "dist/apps/web/commercial-selfhost-dev",
|
||||
"webpackConfig": "bitwarden_license/bit-web/webpack.config.js",
|
||||
"main": "bitwarden_license/bit-web/src/main.ts",
|
||||
"env": {
|
||||
"NODE_ENV": "development",
|
||||
"ENV": "selfhosted"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"executor": "@nx/webpack:dev-server",
|
||||
"defaultConfiguration": "oss-dev",
|
||||
"options": {
|
||||
"buildTarget": "web:build",
|
||||
"host": "localhost",
|
||||
"port": 8080
|
||||
},
|
||||
"configurations": {
|
||||
"oss": {
|
||||
"buildTarget": "web:build:oss"
|
||||
},
|
||||
"oss-dev": {
|
||||
"buildTarget": "web:build:oss-dev"
|
||||
},
|
||||
"commercial": {
|
||||
"buildTarget": "web:build:commercial"
|
||||
},
|
||||
"commercial-dev": {
|
||||
"buildTarget": "web:build:commercial-dev"
|
||||
},
|
||||
"commercial-qa": {
|
||||
"buildTarget": "web:build:commercial-qa"
|
||||
},
|
||||
"commercial-cloud": {
|
||||
"buildTarget": "web:build:commercial-cloud"
|
||||
},
|
||||
"commercial-euprd": {
|
||||
"buildTarget": "web:build:commercial-euprd"
|
||||
},
|
||||
"commercial-euqa": {
|
||||
"buildTarget": "web:build:commercial-euqa"
|
||||
},
|
||||
"commercial-usdev": {
|
||||
"buildTarget": "web:build:commercial-usdev"
|
||||
},
|
||||
"commercial-ee": {
|
||||
"buildTarget": "web:build:commercial-ee"
|
||||
},
|
||||
"oss-selfhost": {
|
||||
"buildTarget": "web:build:oss-selfhost"
|
||||
},
|
||||
"oss-selfhost-dev": {
|
||||
"buildTarget": "web:build:oss-selfhost-dev"
|
||||
},
|
||||
"commercial-selfhost": {
|
||||
"buildTarget": "web:build:commercial-selfhost"
|
||||
},
|
||||
"commercial-selfhost-dev": {
|
||||
"buildTarget": "web:build:commercial-selfhost-dev"
|
||||
}
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "apps/web/jest.config.js"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint",
|
||||
"outputs": ["{options.outputFile}"],
|
||||
"options": {
|
||||
"lintFilePatterns": ["apps/web/**/*.ts", "apps/web/**/*.html"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,7 @@ import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
|
||||
import { OrganizationMetadataServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-metadata.service.abstraction";
|
||||
import { isNotSelfUpgradable, ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
@@ -146,6 +147,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
private groupService: GroupApiService,
|
||||
private collectionService: CollectionService,
|
||||
private billingApiService: BillingApiServiceAbstraction,
|
||||
private organizationMetadataService: OrganizationMetadataServiceAbstraction,
|
||||
protected deleteManagedMemberWarningService: DeleteManagedMemberWarningService,
|
||||
private configService: ConfigService,
|
||||
private organizationUserService: OrganizationUserService,
|
||||
@@ -257,7 +259,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
|
||||
this.billingMetadata$ = combineLatest([this.refreshBillingMetadata$, organization$]).pipe(
|
||||
switchMap(([_, organization]) =>
|
||||
this.billingApiService.getOrganizationBillingMetadata(organization.id),
|
||||
this.organizationMetadataService.getOrganizationMetadata$(organization.id),
|
||||
),
|
||||
takeUntilDestroyed(),
|
||||
shareReplay({ bufferSize: 1, refCount: false }),
|
||||
|
||||
@@ -5,7 +5,6 @@ import { ActivatedRoute } from "@angular/router";
|
||||
import { concatMap, takeUntil, map, lastValueFrom, firstValueFrom } from "rxjs";
|
||||
import { first, tap } from "rxjs/operators";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import {
|
||||
getOrganizationById,
|
||||
OrganizationService,
|
||||
@@ -15,6 +14,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { TwoFactorDuoResponse } from "@bitwarden/common/auth/models/response/two-factor-duo.response";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { TwoFactorApiService } from "@bitwarden/common/auth/two-factor";
|
||||
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
@@ -35,7 +35,7 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent impleme
|
||||
tabbedHeader = false;
|
||||
constructor(
|
||||
dialogService: DialogService,
|
||||
apiService: ApiService,
|
||||
twoFactorApiService: TwoFactorApiService,
|
||||
messagingService: MessagingService,
|
||||
policyService: PolicyService,
|
||||
private route: ActivatedRoute,
|
||||
@@ -47,7 +47,7 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent impleme
|
||||
) {
|
||||
super(
|
||||
dialogService,
|
||||
apiService,
|
||||
twoFactorApiService,
|
||||
messagingService,
|
||||
policyService,
|
||||
billingAccountProfileStateService,
|
||||
@@ -116,7 +116,7 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent impleme
|
||||
}
|
||||
|
||||
protected getTwoFactorProviders() {
|
||||
return this.apiService.getTwoFactorOrganizationProviders(this.organizationId);
|
||||
return this.twoFactorApiService.getTwoFactorOrganizationProviders(this.organizationId);
|
||||
}
|
||||
|
||||
protected filterProvider(type: TwoFactorProviderType): boolean {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { TwoFactorProviderResponse } from "@bitwarden/common/auth/models/response/two-factor-provider.response";
|
||||
import { TwoFactorApiService } from "@bitwarden/common/auth/two-factor";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
@@ -22,12 +23,14 @@ describe("ChangeEmailComponent", () => {
|
||||
let fixture: ComponentFixture<ChangeEmailComponent>;
|
||||
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let twoFactorApiService: MockProxy<TwoFactorApiService>;
|
||||
let accountService: FakeAccountService;
|
||||
let keyService: MockProxy<KeyService>;
|
||||
let kdfConfigService: MockProxy<KdfConfigService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
apiService = mock<ApiService>();
|
||||
twoFactorApiService = mock<TwoFactorApiService>();
|
||||
keyService = mock<KeyService>();
|
||||
kdfConfigService = mock<KdfConfigService>();
|
||||
accountService = mockAccountServiceWith("UserId" as UserId);
|
||||
@@ -37,6 +40,7 @@ describe("ChangeEmailComponent", () => {
|
||||
providers: [
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
{ provide: ApiService, useValue: apiService },
|
||||
{ provide: TwoFactorApiService, useValue: twoFactorApiService },
|
||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||
{ provide: KeyService, useValue: keyService },
|
||||
{ provide: MessagingService, useValue: mock<MessagingService>() },
|
||||
@@ -57,7 +61,7 @@ describe("ChangeEmailComponent", () => {
|
||||
|
||||
describe("ngOnInit", () => {
|
||||
beforeEach(() => {
|
||||
apiService.getTwoFactorProviders.mockResolvedValue({
|
||||
twoFactorApiService.getTwoFactorProviders.mockResolvedValue({
|
||||
data: [{ type: TwoFactorProviderType.Email, enabled: true } as TwoFactorProviderResponse],
|
||||
} as ListResponse<TwoFactorProviderResponse>);
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-p
|
||||
import { EmailTokenRequest } from "@bitwarden/common/auth/models/request/email-token.request";
|
||||
import { EmailRequest } from "@bitwarden/common/auth/models/request/email.request";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { TwoFactorApiService } from "@bitwarden/common/auth/two-factor";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
@@ -37,6 +38,7 @@ export class ChangeEmailComponent implements OnInit {
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private apiService: ApiService,
|
||||
private twoFactorApiService: TwoFactorApiService,
|
||||
private i18nService: I18nService,
|
||||
private keyService: KeyService,
|
||||
private messagingService: MessagingService,
|
||||
@@ -48,7 +50,7 @@ export class ChangeEmailComponent implements OnInit {
|
||||
async ngOnInit() {
|
||||
this.userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
|
||||
const twoFactorProviders = await this.apiService.getTwoFactorProviders();
|
||||
const twoFactorProviders = await this.twoFactorApiService.getTwoFactorProviders();
|
||||
this.showTwoFactorEmailWarning = twoFactorProviders.data.some(
|
||||
(p) => p.type === TwoFactorProviderType.Email && p.enabled,
|
||||
);
|
||||
|
||||
@@ -56,7 +56,9 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
||||
this.profile = await this.apiService.getProfile();
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
this.fingerprintMaterial = userId;
|
||||
const publicKey = await firstValueFrom(this.keyService.userPublicKey$(userId));
|
||||
const publicKey = (await firstValueFrom(
|
||||
this.keyService.userPublicKey$(userId),
|
||||
)) as UserPublicKey;
|
||||
if (publicKey == null) {
|
||||
this.logService.error(
|
||||
"[ProfileComponent] No public key available for the user: " +
|
||||
|
||||
@@ -5,11 +5,11 @@ import { firstValueFrom, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { UserVerificationFormInputComponent } from "@bitwarden/auth/angular";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { SetVerifyDevicesRequest } from "@bitwarden/common/auth/models/request/set-verify-devices.request";
|
||||
import { TwoFactorApiService } from "@bitwarden/common/auth/two-factor";
|
||||
import { Verification } from "@bitwarden/common/auth/types/verification";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -64,7 +64,7 @@ export class SetAccountVerifyDevicesDialogComponent implements OnInit, OnDestroy
|
||||
private userVerificationService: UserVerificationService,
|
||||
private dialogRef: DialogRef,
|
||||
private toastService: ToastService,
|
||||
private apiService: ApiService,
|
||||
private twoFactorApiService: TwoFactorApiService,
|
||||
) {
|
||||
this.accountService.accountVerifyNewDeviceLogin$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
@@ -74,7 +74,7 @@ export class SetAccountVerifyDevicesDialogComponent implements OnInit, OnDestroy
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
const twoFactorProviders = await this.apiService.getTwoFactorProviders();
|
||||
const twoFactorProviders = await this.twoFactorApiService.getTwoFactorProviders();
|
||||
this.has2faConfigured = twoFactorProviders.data.length > 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,13 +6,13 @@ import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from "@angu
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { DisableTwoFactorAuthenticatorRequest } from "@bitwarden/common/auth/models/request/disable-two-factor-authenticator.request";
|
||||
import { UpdateTwoFactorAuthenticatorRequest } from "@bitwarden/common/auth/models/request/update-two-factor-authenticator.request";
|
||||
import { TwoFactorAuthenticatorResponse } from "@bitwarden/common/auth/models/response/two-factor-authenticator.response";
|
||||
import { TwoFactorApiService } from "@bitwarden/common/auth/two-factor";
|
||||
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -92,7 +92,7 @@ export class TwoFactorSetupAuthenticatorComponent
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected data: AuthResponse<TwoFactorAuthenticatorResponse>,
|
||||
private dialogRef: DialogRef,
|
||||
apiService: ApiService,
|
||||
twoFactorApiService: TwoFactorApiService,
|
||||
i18nService: I18nService,
|
||||
userVerificationService: UserVerificationService,
|
||||
private formBuilder: FormBuilder,
|
||||
@@ -104,7 +104,7 @@ export class TwoFactorSetupAuthenticatorComponent
|
||||
protected toastService: ToastService,
|
||||
) {
|
||||
super(
|
||||
apiService,
|
||||
twoFactorApiService,
|
||||
i18nService,
|
||||
platformUtilsService,
|
||||
logService,
|
||||
@@ -154,7 +154,7 @@ export class TwoFactorSetupAuthenticatorComponent
|
||||
request.key = this.key;
|
||||
request.userVerificationToken = this.userVerificationToken;
|
||||
|
||||
const response = await this.apiService.putTwoFactorAuthenticator(request);
|
||||
const response = await this.twoFactorApiService.putTwoFactorAuthenticator(request);
|
||||
await this.processResponse(response);
|
||||
this.onUpdated.emit(true);
|
||||
}
|
||||
@@ -174,7 +174,7 @@ export class TwoFactorSetupAuthenticatorComponent
|
||||
request.type = this.type;
|
||||
request.key = this.key;
|
||||
request.userVerificationToken = this.userVerificationToken;
|
||||
await this.apiService.deleteTwoFactorAuthenticator(request);
|
||||
await this.twoFactorApiService.deleteTwoFactorAuthenticator(request);
|
||||
this.enabled = false;
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
|
||||
@@ -2,11 +2,11 @@ import { CommonModule } from "@angular/common";
|
||||
import { Component, EventEmitter, Inject, OnInit, Output } from "@angular/core";
|
||||
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { UpdateTwoFactorDuoRequest } from "@bitwarden/common/auth/models/request/update-two-factor-duo.request";
|
||||
import { TwoFactorDuoResponse } from "@bitwarden/common/auth/models/response/two-factor-duo.response";
|
||||
import { TwoFactorApiService } from "@bitwarden/common/auth/two-factor";
|
||||
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -63,7 +63,7 @@ export class TwoFactorSetupDuoComponent
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected data: TwoFactorDuoComponentConfig,
|
||||
apiService: ApiService,
|
||||
twoFactorApiService: TwoFactorApiService,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
logService: LogService,
|
||||
@@ -74,7 +74,7 @@ export class TwoFactorSetupDuoComponent
|
||||
protected toastService: ToastService,
|
||||
) {
|
||||
super(
|
||||
apiService,
|
||||
twoFactorApiService,
|
||||
i18nService,
|
||||
platformUtilsService,
|
||||
logService,
|
||||
@@ -139,9 +139,12 @@ export class TwoFactorSetupDuoComponent
|
||||
let response: TwoFactorDuoResponse;
|
||||
|
||||
if (this.organizationId != null) {
|
||||
response = await this.apiService.putTwoFactorOrganizationDuo(this.organizationId, request);
|
||||
response = await this.twoFactorApiService.putTwoFactorOrganizationDuo(
|
||||
this.organizationId,
|
||||
request,
|
||||
);
|
||||
} else {
|
||||
response = await this.apiService.putTwoFactorDuo(request);
|
||||
response = await this.twoFactorApiService.putTwoFactorDuo(request);
|
||||
}
|
||||
|
||||
this.processResponse(response);
|
||||
|
||||
@@ -3,13 +3,13 @@ import { Component, EventEmitter, Inject, OnInit, Output } from "@angular/core";
|
||||
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two-factor-email.request";
|
||||
import { UpdateTwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/update-two-factor-email.request";
|
||||
import { TwoFactorEmailResponse } from "@bitwarden/common/auth/models/response/two-factor-email.response";
|
||||
import { TwoFactorApiService } from "@bitwarden/common/auth/two-factor";
|
||||
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -66,7 +66,7 @@ export class TwoFactorSetupEmailComponent
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected data: AuthResponse<TwoFactorEmailResponse>,
|
||||
apiService: ApiService,
|
||||
twoFactorApiService: TwoFactorApiService,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
logService: LogService,
|
||||
@@ -78,7 +78,7 @@ export class TwoFactorSetupEmailComponent
|
||||
protected toastService: ToastService,
|
||||
) {
|
||||
super(
|
||||
apiService,
|
||||
twoFactorApiService,
|
||||
i18nService,
|
||||
platformUtilsService,
|
||||
logService,
|
||||
@@ -131,7 +131,7 @@ export class TwoFactorSetupEmailComponent
|
||||
sendEmail = async () => {
|
||||
const request = await this.buildRequestModel(TwoFactorEmailRequest);
|
||||
request.email = this.email;
|
||||
this.emailPromise = this.apiService.postTwoFactorEmailSetup(request);
|
||||
this.emailPromise = this.twoFactorApiService.postTwoFactorEmailSetup(request);
|
||||
await this.emailPromise;
|
||||
this.sentEmail = this.email;
|
||||
};
|
||||
@@ -141,7 +141,7 @@ export class TwoFactorSetupEmailComponent
|
||||
request.email = this.email;
|
||||
request.token = this.token;
|
||||
|
||||
const response = await this.apiService.putTwoFactorEmail(request);
|
||||
const response = await this.twoFactorApiService.putTwoFactorEmail(request);
|
||||
await this.processResponse(response);
|
||||
this.onUpdated.emit(true);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Directive, EventEmitter, Output } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
||||
import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request";
|
||||
import { TwoFactorProviderRequest } from "@bitwarden/common/auth/models/request/two-factor-provider.request";
|
||||
import { TwoFactorApiService } from "@bitwarden/common/auth/two-factor";
|
||||
import { AuthResponseBase } from "@bitwarden/common/auth/types/auth-response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -30,7 +30,7 @@ export abstract class TwoFactorSetupMethodBaseComponent {
|
||||
protected componentName = "";
|
||||
|
||||
constructor(
|
||||
protected apiService: ApiService,
|
||||
protected twoFactorApiService: TwoFactorApiService,
|
||||
protected i18nService: I18nService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected logService: LogService,
|
||||
@@ -77,9 +77,12 @@ export abstract class TwoFactorSetupMethodBaseComponent {
|
||||
}
|
||||
request.type = this.type;
|
||||
if (this.organizationId != null) {
|
||||
promise = this.apiService.putTwoFactorOrganizationDisable(this.organizationId, request);
|
||||
promise = this.twoFactorApiService.putTwoFactorOrganizationDisable(
|
||||
this.organizationId,
|
||||
request,
|
||||
);
|
||||
} else {
|
||||
promise = this.apiService.putTwoFactorDisable(request);
|
||||
promise = this.twoFactorApiService.putTwoFactorDisable(request);
|
||||
}
|
||||
await promise;
|
||||
this.enabled = false;
|
||||
@@ -111,9 +114,9 @@ export abstract class TwoFactorSetupMethodBaseComponent {
|
||||
}
|
||||
request.type = this.type;
|
||||
if (this.organizationId != null) {
|
||||
await this.apiService.putTwoFactorOrganizationDisable(this.organizationId, request);
|
||||
await this.twoFactorApiService.putTwoFactorOrganizationDisable(this.organizationId, request);
|
||||
} else {
|
||||
await this.apiService.putTwoFactorDisable(request);
|
||||
await this.twoFactorApiService.putTwoFactorDisable(request);
|
||||
}
|
||||
this.enabled = false;
|
||||
this.toastService.showToast({
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Component, Inject, NgZone } from "@angular/core";
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from "@angular/forms";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request";
|
||||
@@ -13,6 +12,7 @@ import {
|
||||
ChallengeResponse,
|
||||
TwoFactorWebAuthnResponse,
|
||||
} from "@bitwarden/common/auth/models/response/two-factor-web-authn.response";
|
||||
import { TwoFactorApiService } from "@bitwarden/common/auth/two-factor";
|
||||
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -79,7 +79,7 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected data: AuthResponse<TwoFactorWebAuthnResponse>,
|
||||
private dialogRef: DialogRef,
|
||||
apiService: ApiService,
|
||||
twoFactorApiService: TwoFactorApiService,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
private ngZone: NgZone,
|
||||
@@ -89,7 +89,7 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom
|
||||
toastService: ToastService,
|
||||
) {
|
||||
super(
|
||||
apiService,
|
||||
twoFactorApiService,
|
||||
i18nService,
|
||||
platformUtilsService,
|
||||
logService,
|
||||
@@ -127,7 +127,7 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom
|
||||
request.id = this.keyIdAvailable;
|
||||
request.name = this.formGroup.value.name || "";
|
||||
|
||||
const response = await this.apiService.putTwoFactorWebAuthn(request);
|
||||
const response = await this.twoFactorApiService.putTwoFactorWebAuthn(request);
|
||||
this.processResponse(response);
|
||||
this.toastService.showToast({
|
||||
title: this.i18nService.t("success"),
|
||||
@@ -163,7 +163,7 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom
|
||||
const request = await this.buildRequestModel(UpdateTwoFactorWebAuthnDeleteRequest);
|
||||
request.id = key.id;
|
||||
try {
|
||||
key.removePromise = this.apiService.deleteTwoFactorWebAuthn(request);
|
||||
key.removePromise = this.twoFactorApiService.deleteTwoFactorWebAuthn(request);
|
||||
const response = await key.removePromise;
|
||||
key.removePromise = null;
|
||||
await this.processResponse(response);
|
||||
@@ -177,7 +177,7 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom
|
||||
return;
|
||||
}
|
||||
const request = await this.buildRequestModel(SecretVerificationRequest);
|
||||
this.challengePromise = this.apiService.getTwoFactorWebAuthnChallenge(request);
|
||||
this.challengePromise = this.twoFactorApiService.getTwoFactorWebAuthnChallenge(request);
|
||||
const challenge = await this.challengePromise;
|
||||
this.readDevice(challenge);
|
||||
};
|
||||
|
||||
@@ -9,11 +9,11 @@ import {
|
||||
} from "@angular/forms";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { UpdateTwoFactorYubikeyOtpRequest } from "@bitwarden/common/auth/models/request/update-two-factor-yubikey-otp.request";
|
||||
import { TwoFactorYubiKeyResponse } from "@bitwarden/common/auth/models/response/two-factor-yubi-key.response";
|
||||
import { TwoFactorApiService } from "@bitwarden/common/auth/two-factor";
|
||||
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -93,7 +93,7 @@ export class TwoFactorSetupYubiKeyComponent
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected data: AuthResponse<TwoFactorYubiKeyResponse>,
|
||||
apiService: ApiService,
|
||||
twoFactorApiService: TwoFactorApiService,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
logService: LogService,
|
||||
@@ -103,7 +103,7 @@ export class TwoFactorSetupYubiKeyComponent
|
||||
protected toastService: ToastService,
|
||||
) {
|
||||
super(
|
||||
apiService,
|
||||
twoFactorApiService,
|
||||
i18nService,
|
||||
platformUtilsService,
|
||||
logService,
|
||||
@@ -176,7 +176,7 @@ export class TwoFactorSetupYubiKeyComponent
|
||||
request.key5 = keys != null && keys.length > 4 ? (keys[4]?.key ?? "") : "";
|
||||
request.nfc = this.formGroup.value.anyKeyHasNfc ?? false;
|
||||
|
||||
this.processResponse(await this.apiService.putTwoFactorYubiKey(request));
|
||||
this.processResponse(await this.twoFactorApiService.putTwoFactorYubiKey(request));
|
||||
this.refreshFormArrayData();
|
||||
this.toastService.showToast({
|
||||
title: this.i18nService.t("success"),
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
} from "rxjs";
|
||||
|
||||
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
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";
|
||||
@@ -26,6 +25,7 @@ import { TwoFactorWebAuthnResponse } from "@bitwarden/common/auth/models/respons
|
||||
import { TwoFactorYubiKeyResponse } from "@bitwarden/common/auth/models/response/two-factor-yubi-key.response";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { TwoFactorProviders } from "@bitwarden/common/auth/services/two-factor.service";
|
||||
import { TwoFactorApiService } from "@bitwarden/common/auth/two-factor";
|
||||
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
@@ -68,7 +68,7 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
|
||||
|
||||
constructor(
|
||||
protected dialogService: DialogService,
|
||||
protected apiService: ApiService,
|
||||
protected twoFactorApiService: TwoFactorApiService,
|
||||
protected messagingService: MessagingService,
|
||||
protected policyService: PolicyService,
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
@@ -270,7 +270,7 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
protected getTwoFactorProviders() {
|
||||
return this.apiService.getTwoFactorProviders();
|
||||
return this.twoFactorApiService.getTwoFactorProviders();
|
||||
}
|
||||
|
||||
protected filterProvider(type: TwoFactorProviderType): boolean {
|
||||
|
||||
@@ -2,11 +2,11 @@ import { Component, EventEmitter, Inject, Output } from "@angular/core";
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from "@angular/forms";
|
||||
|
||||
import { UserVerificationFormInputComponent } from "@bitwarden/auth/angular";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
||||
import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request";
|
||||
import { TwoFactorApiService } from "@bitwarden/common/auth/two-factor";
|
||||
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
|
||||
import { TwoFactorResponse } from "@bitwarden/common/auth/types/two-factor-response";
|
||||
import { Verification } from "@bitwarden/common/auth/types/verification";
|
||||
@@ -55,7 +55,7 @@ export class TwoFactorVerifyComponent {
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected data: TwoFactorVerifyDialogData,
|
||||
private dialogRef: DialogRef,
|
||||
private apiService: ApiService,
|
||||
private twoFactorApiService: TwoFactorApiService,
|
||||
private i18nService: I18nService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
) {
|
||||
@@ -116,22 +116,22 @@ export class TwoFactorVerifyComponent {
|
||||
private apiCall(request: SecretVerificationRequest): Promise<TwoFactorResponse> {
|
||||
switch (this.type) {
|
||||
case -1 as TwoFactorProviderType:
|
||||
return this.apiService.getTwoFactorRecover(request);
|
||||
return this.twoFactorApiService.getTwoFactorRecover(request);
|
||||
case TwoFactorProviderType.Duo:
|
||||
case TwoFactorProviderType.OrganizationDuo:
|
||||
if (this.organizationId != null) {
|
||||
return this.apiService.getTwoFactorOrganizationDuo(this.organizationId, request);
|
||||
return this.twoFactorApiService.getTwoFactorOrganizationDuo(this.organizationId, request);
|
||||
} else {
|
||||
return this.apiService.getTwoFactorDuo(request);
|
||||
return this.twoFactorApiService.getTwoFactorDuo(request);
|
||||
}
|
||||
case TwoFactorProviderType.Email:
|
||||
return this.apiService.getTwoFactorEmail(request);
|
||||
return this.twoFactorApiService.getTwoFactorEmail(request);
|
||||
case TwoFactorProviderType.WebAuthn:
|
||||
return this.apiService.getTwoFactorWebAuthn(request);
|
||||
return this.twoFactorApiService.getTwoFactorWebAuthn(request);
|
||||
case TwoFactorProviderType.Authenticator:
|
||||
return this.apiService.getTwoFactorAuthenticator(request);
|
||||
return this.twoFactorApiService.getTwoFactorAuthenticator(request);
|
||||
case TwoFactorProviderType.Yubikey:
|
||||
return this.apiService.getTwoFactorYubiKey(request);
|
||||
return this.twoFactorApiService.getTwoFactorYubiKey(request);
|
||||
default:
|
||||
throw new Error(`Unknown two-factor type: ${this.type}`);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { AccountPaymentDetailsComponent } from "@bitwarden/web-vault/app/billing/individual/payment-details/account-payment-details.component";
|
||||
|
||||
import { BillingHistoryViewComponent } from "./billing-history-view.component";
|
||||
import { PremiumVNextComponent } from "./premium/premium-vnext.component";
|
||||
import { PremiumComponent } from "./premium/premium.component";
|
||||
import { SubscriptionComponent } from "./subscription.component";
|
||||
import { UserSubscriptionComponent } from "./user-subscription.component";
|
||||
@@ -20,11 +23,15 @@ const routes: Routes = [
|
||||
component: UserSubscriptionComponent,
|
||||
data: { titleId: "premiumMembership" },
|
||||
},
|
||||
{
|
||||
path: "premium",
|
||||
component: PremiumComponent,
|
||||
data: { titleId: "goPremium" },
|
||||
},
|
||||
...featureFlaggedRoute({
|
||||
defaultComponent: PremiumComponent,
|
||||
flaggedComponent: PremiumVNextComponent,
|
||||
featureFlag: FeatureFlag.PM24033PremiumUpgradeNewDesign,
|
||||
routeOptions: {
|
||||
data: { titleId: "goPremium" },
|
||||
path: "premium",
|
||||
},
|
||||
}),
|
||||
{
|
||||
path: "payment-details",
|
||||
component: AccountPaymentDetailsComponent,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { PricingCardComponent } from "@bitwarden/pricing";
|
||||
import {
|
||||
EnterBillingAddressComponent,
|
||||
EnterPaymentMethodComponent,
|
||||
@@ -21,6 +22,7 @@ import { UserSubscriptionComponent } from "./user-subscription.component";
|
||||
HeaderModule,
|
||||
EnterPaymentMethodComponent,
|
||||
EnterBillingAddressComponent,
|
||||
PricingCardComponent,
|
||||
],
|
||||
declarations: [
|
||||
SubscriptionComponent,
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<div class="tw-max-w-3xl tw-mx-auto">
|
||||
<bit-section *ngIf="shouldShowNewDesign$ | async">
|
||||
<div class="tw-text-center">
|
||||
<div class="tw-mt-8 tw-mb-6">
|
||||
<span bitBadge variant="secondary" [truncate]="false">
|
||||
{{ "bitwardenFreeplanMessage" | i18n }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h2 *ngIf="!isSelfHost" class="tw-mt-2 tw-text-4xl">
|
||||
{{ "upgradeCompleteSecurity" | i18n }}
|
||||
</h2>
|
||||
<p class="tw-text-muted tw-mb-6 tw-mt-4">
|
||||
{{ "individualUpgradeDescriptionMessage" | i18n }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Two-Card Layout -->
|
||||
<div class="tw-grid tw-grid-cols-1 md:tw-grid-cols-2 tw-gap-6 tw-mt-6 tw-justify-center">
|
||||
<!-- Premium Card -->
|
||||
<div>
|
||||
@if (premiumCardData$ | async; as premiumData) {
|
||||
<billing-pricing-card
|
||||
[tagline]="'planDescPremium' | i18n"
|
||||
[price]="{ amount: premiumData.price, cadence: 'monthly' }"
|
||||
[button]="{ type: 'primary', text: ('upgradeToPremium' | i18n) }"
|
||||
[features]="premiumData.features"
|
||||
(buttonClick)="openUpgradeDialog('Premium')"
|
||||
>
|
||||
<h3 slot="title" bitTypography="h3" class="tw-m-0">{{ "premium" | i18n }}</h3>
|
||||
</billing-pricing-card>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Families Card -->
|
||||
<div>
|
||||
@if (familiesCardData$ | async; as familiesData) {
|
||||
<billing-pricing-card
|
||||
[tagline]="'planDescFamiliesV2' | i18n"
|
||||
[price]="{ amount: familiesData.price, cadence: 'monthly' }"
|
||||
[button]="{ type: 'secondary', text: ('upgradeToFamilies' | i18n) }"
|
||||
[features]="familiesData.features"
|
||||
(buttonClick)="openUpgradeDialog('Families')"
|
||||
>
|
||||
<h3 slot="title" bitTypography="h3" class="tw-m-0">{{ "families" | i18n }}</h3>
|
||||
</billing-pricing-card>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Business Plans Link -->
|
||||
<div class="tw-text-center tw-mt-6">
|
||||
<p class="tw-text-muted tw-mb-2 tw-italic">
|
||||
{{ "individualUpgradeTaxInformationMessage" | i18n }}
|
||||
</p>
|
||||
<a
|
||||
bitLink
|
||||
linkType="primary"
|
||||
href="https://bitwarden.com/pricing/business/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{{ "viewbusinessplans" | i18n }}
|
||||
<i class="bwi bwi-external-link tw-ml-1" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
</bit-section>
|
||||
</div>
|
||||
@@ -0,0 +1,182 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, DestroyRef, inject } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { combineLatest, firstValueFrom, map, Observable, of, shareReplay, switchMap } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import {
|
||||
DialogService,
|
||||
ToastService,
|
||||
SectionComponent,
|
||||
BadgeModule,
|
||||
TypographyModule,
|
||||
LinkModule,
|
||||
} from "@bitwarden/components";
|
||||
import { PricingCardComponent } from "@bitwarden/pricing";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { SubscriptionPricingService } from "../../services/subscription-pricing.service";
|
||||
import { BitwardenSubscriber, mapAccountToSubscriber } from "../../types";
|
||||
import {
|
||||
PersonalSubscriptionPricingTier,
|
||||
PersonalSubscriptionPricingTierIds,
|
||||
} from "../../types/subscription-pricing-tier";
|
||||
import {
|
||||
UnifiedUpgradeDialogComponent,
|
||||
UnifiedUpgradeDialogParams,
|
||||
UnifiedUpgradeDialogResult,
|
||||
UnifiedUpgradeDialogStatus,
|
||||
UnifiedUpgradeDialogStep,
|
||||
} from "../upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component";
|
||||
|
||||
@Component({
|
||||
templateUrl: "./premium-vnext.component.html",
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
SectionComponent,
|
||||
BadgeModule,
|
||||
TypographyModule,
|
||||
LinkModule,
|
||||
I18nPipe,
|
||||
PricingCardComponent,
|
||||
],
|
||||
})
|
||||
export class PremiumVNextComponent {
|
||||
protected hasPremiumFromAnyOrganization$: Observable<boolean>;
|
||||
protected hasPremiumPersonally$: Observable<boolean>;
|
||||
protected shouldShowNewDesign$: Observable<boolean>;
|
||||
protected personalPricingTiers$: Observable<PersonalSubscriptionPricingTier[]>;
|
||||
protected premiumCardData$: Observable<{
|
||||
tier: PersonalSubscriptionPricingTier | undefined;
|
||||
price: number;
|
||||
features: string[];
|
||||
}>;
|
||||
protected familiesCardData$: Observable<{
|
||||
tier: PersonalSubscriptionPricingTier | undefined;
|
||||
price: number;
|
||||
features: string[];
|
||||
}>;
|
||||
|
||||
protected subscriber!: BitwardenSubscriber;
|
||||
protected isSelfHost = false;
|
||||
private destroyRef = inject(DestroyRef);
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private i18nService: I18nService,
|
||||
private apiService: ApiService,
|
||||
private dialogService: DialogService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private syncService: SyncService,
|
||||
private toastService: ToastService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private subscriptionPricingService: SubscriptionPricingService,
|
||||
) {
|
||||
this.isSelfHost = this.platformUtilsService.isSelfHost();
|
||||
|
||||
this.hasPremiumFromAnyOrganization$ = this.accountService.activeAccount$.pipe(
|
||||
switchMap((account) =>
|
||||
account
|
||||
? this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$(account.id)
|
||||
: of(false),
|
||||
),
|
||||
);
|
||||
|
||||
this.hasPremiumPersonally$ = this.accountService.activeAccount$.pipe(
|
||||
switchMap((account) =>
|
||||
account
|
||||
? this.billingAccountProfileStateService.hasPremiumPersonally$(account.id)
|
||||
: of(false),
|
||||
),
|
||||
);
|
||||
|
||||
this.accountService.activeAccount$
|
||||
.pipe(mapAccountToSubscriber, takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((subscriber) => {
|
||||
this.subscriber = subscriber;
|
||||
});
|
||||
|
||||
this.shouldShowNewDesign$ = combineLatest([
|
||||
this.hasPremiumFromAnyOrganization$,
|
||||
this.hasPremiumPersonally$,
|
||||
]).pipe(map(([hasOrgPremium, hasPersonalPremium]) => !hasOrgPremium && !hasPersonalPremium));
|
||||
|
||||
this.personalPricingTiers$ =
|
||||
this.subscriptionPricingService.getPersonalSubscriptionPricingTiers$();
|
||||
|
||||
this.premiumCardData$ = this.personalPricingTiers$.pipe(
|
||||
map((tiers) => {
|
||||
const tier = tiers.find((t) => t.id === PersonalSubscriptionPricingTierIds.Premium);
|
||||
return {
|
||||
tier,
|
||||
price:
|
||||
tier?.passwordManager.type === "standalone"
|
||||
? Number((tier.passwordManager.annualPrice / 12).toFixed(2))
|
||||
: 0,
|
||||
features: tier?.passwordManager.features.map((f) => f.value) || [],
|
||||
};
|
||||
}),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
this.familiesCardData$ = this.personalPricingTiers$.pipe(
|
||||
map((tiers) => {
|
||||
const tier = tiers.find((t) => t.id === PersonalSubscriptionPricingTierIds.Families);
|
||||
return {
|
||||
tier,
|
||||
price:
|
||||
tier?.passwordManager.type === "packaged"
|
||||
? Number((tier.passwordManager.annualPrice / 12).toFixed(2))
|
||||
: 0,
|
||||
features: tier?.passwordManager.features.map((f) => f.value) || [],
|
||||
};
|
||||
}),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
}
|
||||
|
||||
finalizeUpgrade = async () => {
|
||||
await this.apiService.refreshIdentityToken();
|
||||
await this.syncService.fullSync(true);
|
||||
};
|
||||
|
||||
protected async openUpgradeDialog(planType: "Premium" | "Families"): Promise<void> {
|
||||
const account = await firstValueFrom(this.accountService.activeAccount$);
|
||||
if (!account) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedPlan =
|
||||
planType === "Premium"
|
||||
? PersonalSubscriptionPricingTierIds.Premium
|
||||
: PersonalSubscriptionPricingTierIds.Families;
|
||||
|
||||
const dialogParams: UnifiedUpgradeDialogParams = {
|
||||
account,
|
||||
initialStep: UnifiedUpgradeDialogStep.Payment,
|
||||
selectedPlan: selectedPlan,
|
||||
redirectOnCompletion: true,
|
||||
};
|
||||
|
||||
const dialogRef = UnifiedUpgradeDialogComponent.open(this.dialogService, {
|
||||
data: dialogParams,
|
||||
});
|
||||
|
||||
dialogRef.closed
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((result: UnifiedUpgradeDialogResult | undefined) => {
|
||||
if (
|
||||
result?.status === UnifiedUpgradeDialogStatus.UpgradedToPremium ||
|
||||
result?.status === UnifiedUpgradeDialogStatus.UpgradedToFamilies
|
||||
) {
|
||||
void this.finalizeUpgrade();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,137 +1,132 @@
|
||||
<bit-section>
|
||||
<h2 *ngIf="!isSelfHost" bitTypography="h2">{{ "goPremium" | i18n }}</h2>
|
||||
<bit-callout
|
||||
type="info"
|
||||
*ngIf="hasPremiumFromAnyOrganization$ | async"
|
||||
title="{{ 'youHavePremiumAccess' | i18n }}"
|
||||
icon="bwi bwi-star-f"
|
||||
>
|
||||
{{ "alreadyPremiumFromOrg" | i18n }}
|
||||
</bit-callout>
|
||||
<bit-callout type="success">
|
||||
<p>{{ "premiumUpgradeUnlockFeatures" | i18n }}</p>
|
||||
<ul class="bwi-ul">
|
||||
<li>
|
||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||
{{ "premiumSignUpStorage" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||
{{ "premiumSignUpTwoStepOptions" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||
{{ "premiumSignUpEmergency" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||
{{ "premiumSignUpReports" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||
{{ "premiumSignUpTotp" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||
{{ "premiumSignUpSupport" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||
{{ "premiumSignUpFuture" | i18n }}
|
||||
</li>
|
||||
</ul>
|
||||
<p bitTypography="body1" [ngClass]="{ 'tw-mb-0': !isSelfHost }">
|
||||
{{
|
||||
"premiumPriceWithFamilyPlan" | i18n: (premiumPrice | currency: "$") : familyPlanMaxUserCount
|
||||
}}
|
||||
<bit-container>
|
||||
<bit-section>
|
||||
<h2 *ngIf="!isSelfHost" bitTypography="h2">{{ "goPremium" | i18n }}</h2>
|
||||
<bit-callout
|
||||
type="info"
|
||||
*ngIf="hasPremiumFromAnyOrganization$ | async"
|
||||
title="{{ 'youHavePremiumAccess' | i18n }}"
|
||||
icon="bwi bwi-star-f"
|
||||
>
|
||||
{{ "alreadyPremiumFromOrg" | i18n }}
|
||||
</bit-callout>
|
||||
<bit-callout type="success">
|
||||
<p>{{ "premiumUpgradeUnlockFeatures" | i18n }}</p>
|
||||
<ul class="bwi-ul">
|
||||
<li>
|
||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||
{{ "premiumSignUpStorage" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||
{{ "premiumSignUpTwoStepOptions" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||
{{ "premiumSignUpEmergency" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||
{{ "premiumSignUpReports" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||
{{ "premiumSignUpTotp" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||
{{ "premiumSignUpSupport" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||
{{ "premiumSignUpFuture" | i18n }}
|
||||
</li>
|
||||
</ul>
|
||||
<p bitTypography="body1" [ngClass]="{ 'tw-mb-0': !isSelfHost }">
|
||||
{{
|
||||
"premiumPriceWithFamilyPlan"
|
||||
| i18n: (premiumPrice | currency: "$") : familyPlanMaxUserCount
|
||||
}}
|
||||
<a
|
||||
bitLink
|
||||
linkType="primary"
|
||||
routerLink="/create-organization"
|
||||
[queryParams]="{ plan: 'families' }"
|
||||
>
|
||||
{{ "bitwardenFamiliesPlan" | i18n }}
|
||||
</a>
|
||||
</p>
|
||||
<a
|
||||
bitLink
|
||||
linkType="primary"
|
||||
routerLink="/create-organization"
|
||||
[queryParams]="{ plan: 'families' }"
|
||||
bitButton
|
||||
href="{{ premiumURL }}"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
buttonType="secondary"
|
||||
*ngIf="isSelfHost"
|
||||
>
|
||||
{{ "bitwardenFamiliesPlan" | i18n }}
|
||||
{{ "purchasePremium" | i18n }}
|
||||
</a>
|
||||
</p>
|
||||
<a
|
||||
bitButton
|
||||
href="{{ premiumURL }}"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
buttonType="secondary"
|
||||
*ngIf="isSelfHost"
|
||||
>
|
||||
{{ "purchasePremium" | i18n }}
|
||||
</a>
|
||||
</bit-callout>
|
||||
</bit-section>
|
||||
<bit-section *ngIf="isSelfHost">
|
||||
<individual-self-hosting-license-uploader
|
||||
(onLicenseFileUploaded)="onLicenseFileSelectedChanged()"
|
||||
/>
|
||||
</bit-section>
|
||||
<form *ngIf="!isSelfHost" [formGroup]="formGroup" [bitSubmit]="submitPayment">
|
||||
<bit-section>
|
||||
<h2 bitTypography="h2">{{ "addons" | i18n }}</h2>
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<bit-form-field class="tw-col-span-6">
|
||||
<bit-label>{{ "additionalStorageGb" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
formControlName="additionalStorage"
|
||||
type="number"
|
||||
step="1"
|
||||
placeholder="{{ 'additionalStorageGbDesc' | i18n }}"
|
||||
/>
|
||||
<bit-hint>{{
|
||||
"additionalStorageIntervalDesc"
|
||||
| i18n: "1 GB" : (storageGBPrice | currency: "$") : ("year" | i18n)
|
||||
}}</bit-hint>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
</bit-callout>
|
||||
</bit-section>
|
||||
<bit-section>
|
||||
<h2 bitTypography="h2">{{ "summary" | i18n }}</h2>
|
||||
{{ "premiumMembership" | i18n }}: {{ premiumPrice | currency: "$" }} <br />
|
||||
{{ "additionalStorageGb" | i18n }}: {{ formGroup.value.additionalStorage || 0 }} GB ×
|
||||
{{ storageGBPrice | currency: "$" }} =
|
||||
{{ additionalStorageCost | currency: "$" }}
|
||||
<hr class="tw-my-3" />
|
||||
<bit-section *ngIf="isSelfHost">
|
||||
<individual-self-hosting-license-uploader
|
||||
(onLicenseFileUploaded)="onLicenseFileSelectedChanged()"
|
||||
/>
|
||||
</bit-section>
|
||||
<bit-section>
|
||||
<h3 bitTypography="h2">{{ "paymentInformation" | i18n }}</h3>
|
||||
<div class="tw-mb-4">
|
||||
<app-enter-payment-method
|
||||
[group]="formGroup.controls.paymentMethod"
|
||||
[showBankAccount]="false"
|
||||
[showAccountCredit]="true"
|
||||
[hasEnoughAccountCredit]="hasEnoughAccountCredit$ | async"
|
||||
>
|
||||
</app-enter-payment-method>
|
||||
<app-enter-billing-address
|
||||
[group]="formGroup.controls.billingAddress"
|
||||
[scenario]="{ type: 'checkout', supportsTaxId: false }"
|
||||
>
|
||||
</app-enter-billing-address>
|
||||
</div>
|
||||
<div class="tw-mb-4">
|
||||
<div class="tw-text-muted tw-text-sm tw-flex tw-flex-col">
|
||||
<span>{{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }}</span>
|
||||
<span>{{ "estimatedTax" | i18n }}: {{ estimatedTax | currency: "USD $" }}</span>
|
||||
<form *ngIf="!isSelfHost" [formGroup]="formGroup" [bitSubmit]="submitPayment">
|
||||
<bit-section>
|
||||
<h2 bitTypography="h2">{{ "addons" | i18n }}</h2>
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<bit-form-field class="tw-col-span-6">
|
||||
<bit-label>{{ "additionalStorageGb" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
formControlName="additionalStorage"
|
||||
type="number"
|
||||
step="1"
|
||||
placeholder="{{ 'additionalStorageGbDesc' | i18n }}"
|
||||
/>
|
||||
<bit-hint>{{
|
||||
"additionalStorageIntervalDesc"
|
||||
| i18n: "1 GB" : (storageGBPrice | currency: "$") : ("year" | i18n)
|
||||
}}</bit-hint>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="tw-my-1 tw-w-1/4 tw-ml-0" />
|
||||
<p bitTypography="body1">
|
||||
<strong>{{ "total" | i18n }}:</strong> {{ total | currency: "USD $" }}/{{ "year" | i18n }}
|
||||
</p>
|
||||
<button
|
||||
type="submit"
|
||||
buttonType="primary"
|
||||
bitButton
|
||||
bitFormButton
|
||||
[disabled]="!(hasEnoughAccountCredit$ | async)"
|
||||
>
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
</bit-section>
|
||||
</form>
|
||||
</bit-section>
|
||||
<bit-section>
|
||||
<h2 bitTypography="h2">{{ "summary" | i18n }}</h2>
|
||||
{{ "premiumMembership" | i18n }}: {{ premiumPrice | currency: "$" }} <br />
|
||||
{{ "additionalStorageGb" | i18n }}: {{ formGroup.value.additionalStorage || 0 }} GB ×
|
||||
{{ storageGBPrice | currency: "$" }} =
|
||||
{{ additionalStorageCost | currency: "$" }}
|
||||
<hr class="tw-my-3" />
|
||||
</bit-section>
|
||||
<bit-section>
|
||||
<h3 bitTypography="h2">{{ "paymentInformation" | i18n }}</h3>
|
||||
<div class="tw-mb-4">
|
||||
<app-enter-payment-method
|
||||
[group]="formGroup.controls.paymentMethod"
|
||||
[showBankAccount]="false"
|
||||
>
|
||||
</app-enter-payment-method>
|
||||
<app-enter-billing-address
|
||||
[group]="formGroup.controls.billingAddress"
|
||||
[scenario]="{ type: 'checkout', supportsTaxId: false }"
|
||||
>
|
||||
</app-enter-billing-address>
|
||||
</div>
|
||||
<div class="tw-mb-4">
|
||||
<div class="tw-text-muted tw-text-sm tw-flex tw-flex-col">
|
||||
<span>{{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }}</span>
|
||||
<span>{{ "estimatedTax" | i18n }}: {{ estimatedTax | currency: "USD $" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="tw-my-1 tw-w-1/4 tw-ml-0" />
|
||||
<p bitTypography="body1">
|
||||
<strong>{{ "total" | i18n }}:</strong> {{ total | currency: "USD $" }}/{{ "year" | i18n }}
|
||||
</p>
|
||||
<button type="submit" buttonType="primary" bitButton bitFormButton>
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
</bit-section>
|
||||
</form>
|
||||
</bit-container>
|
||||
|
||||
@@ -8,6 +8,4 @@
|
||||
</bit-tab-nav-bar>
|
||||
</app-header>
|
||||
|
||||
<bit-container>
|
||||
<router-outlet></router-outlet>
|
||||
</bit-container>
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
@if (step() == PlanSelectionStep) {
|
||||
<app-upgrade-account (planSelected)="onPlanSelected($event)" (closeClicked)="onCloseClicked()" />
|
||||
} @else if (step() == PaymentStep && selectedPlan() !== null) {
|
||||
<app-upgrade-account
|
||||
[dialogTitleMessageOverride]="planSelectionStepTitleOverride()"
|
||||
[hideContinueWithoutUpgradingButton]="hideContinueWithoutUpgradingButton()"
|
||||
(planSelected)="onPlanSelected($event)"
|
||||
(closeClicked)="onCloseClicked()"
|
||||
/>
|
||||
} @else if (step() == PaymentStep && selectedPlan() !== null && account() !== null) {
|
||||
<app-upgrade-payment
|
||||
[selectedPlanId]="selectedPlan()"
|
||||
[account]="account()"
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
import { Component, input, output } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { DIALOG_DATA, DialogRef } from "@bitwarden/components";
|
||||
|
||||
import {
|
||||
PersonalSubscriptionPricingTierId,
|
||||
PersonalSubscriptionPricingTierIds,
|
||||
} from "../../../types/subscription-pricing-tier";
|
||||
import {
|
||||
UpgradeAccountComponent,
|
||||
UpgradeAccountStatus,
|
||||
} from "../upgrade-account/upgrade-account.component";
|
||||
import {
|
||||
UpgradePaymentComponent,
|
||||
UpgradePaymentResult,
|
||||
} from "../upgrade-payment/upgrade-payment.component";
|
||||
|
||||
import {
|
||||
UnifiedUpgradeDialogComponent,
|
||||
UnifiedUpgradeDialogParams,
|
||||
UnifiedUpgradeDialogStep,
|
||||
} from "./unified-upgrade-dialog.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-upgrade-account",
|
||||
template: "",
|
||||
standalone: true,
|
||||
})
|
||||
class MockUpgradeAccountComponent {
|
||||
dialogTitleMessageOverride = input<string | null>(null);
|
||||
hideContinueWithoutUpgradingButton = input<boolean>(false);
|
||||
planSelected = output<PersonalSubscriptionPricingTierId>();
|
||||
closeClicked = output<UpgradeAccountStatus>();
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "app-upgrade-payment",
|
||||
template: "",
|
||||
standalone: true,
|
||||
})
|
||||
class MockUpgradePaymentComponent {
|
||||
selectedPlanId = input<PersonalSubscriptionPricingTierId | null>(null);
|
||||
account = input<Account | null>(null);
|
||||
goBack = output<void>();
|
||||
complete = output<UpgradePaymentResult>();
|
||||
}
|
||||
|
||||
describe("UnifiedUpgradeDialogComponent", () => {
|
||||
let component: UnifiedUpgradeDialogComponent;
|
||||
let fixture: ComponentFixture<UnifiedUpgradeDialogComponent>;
|
||||
const mockDialogRef = mock<DialogRef>();
|
||||
|
||||
const mockAccount: Account = {
|
||||
id: "user-id" as UserId,
|
||||
email: "test@example.com",
|
||||
emailVerified: true,
|
||||
name: "Test User",
|
||||
};
|
||||
|
||||
const defaultDialogData: UnifiedUpgradeDialogParams = {
|
||||
account: mockAccount,
|
||||
initialStep: null,
|
||||
selectedPlan: null,
|
||||
planSelectionStepTitleOverride: null,
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [NoopAnimationsModule, UnifiedUpgradeDialogComponent],
|
||||
providers: [
|
||||
{ provide: DialogRef, useValue: mockDialogRef },
|
||||
{ provide: DIALOG_DATA, useValue: defaultDialogData },
|
||||
],
|
||||
})
|
||||
.overrideComponent(UnifiedUpgradeDialogComponent, {
|
||||
remove: {
|
||||
imports: [UpgradeAccountComponent, UpgradePaymentComponent],
|
||||
},
|
||||
add: {
|
||||
imports: [MockUpgradeAccountComponent, MockUpgradePaymentComponent],
|
||||
},
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(UnifiedUpgradeDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should create", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should initialize with default values", () => {
|
||||
expect(component["step"]()).toBe(UnifiedUpgradeDialogStep.PlanSelection);
|
||||
expect(component["selectedPlan"]()).toBeNull();
|
||||
expect(component["account"]()).toEqual(mockAccount);
|
||||
expect(component["planSelectionStepTitleOverride"]()).toBeNull();
|
||||
});
|
||||
|
||||
it("should initialize with custom initial step", async () => {
|
||||
TestBed.resetTestingModule();
|
||||
|
||||
const customDialogData: UnifiedUpgradeDialogParams = {
|
||||
account: mockAccount,
|
||||
initialStep: UnifiedUpgradeDialogStep.Payment,
|
||||
selectedPlan: PersonalSubscriptionPricingTierIds.Premium,
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [NoopAnimationsModule, UnifiedUpgradeDialogComponent],
|
||||
providers: [
|
||||
{ provide: DialogRef, useValue: mockDialogRef },
|
||||
{ provide: DIALOG_DATA, useValue: customDialogData },
|
||||
],
|
||||
})
|
||||
.overrideComponent(UnifiedUpgradeDialogComponent, {
|
||||
remove: {
|
||||
imports: [UpgradeAccountComponent, UpgradePaymentComponent],
|
||||
},
|
||||
add: {
|
||||
imports: [MockUpgradeAccountComponent, MockUpgradePaymentComponent],
|
||||
},
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
const customFixture = TestBed.createComponent(UnifiedUpgradeDialogComponent);
|
||||
const customComponent = customFixture.componentInstance;
|
||||
customFixture.detectChanges();
|
||||
|
||||
expect(customComponent["step"]()).toBe(UnifiedUpgradeDialogStep.Payment);
|
||||
expect(customComponent["selectedPlan"]()).toBe(PersonalSubscriptionPricingTierIds.Premium);
|
||||
});
|
||||
|
||||
describe("custom dialog title", () => {
|
||||
it("should use null as default when no override is provided", () => {
|
||||
expect(component["planSelectionStepTitleOverride"]()).toBeNull();
|
||||
});
|
||||
|
||||
it("should use custom title when provided in dialog config", async () => {
|
||||
TestBed.resetTestingModule();
|
||||
|
||||
const customDialogData: UnifiedUpgradeDialogParams = {
|
||||
account: mockAccount,
|
||||
initialStep: UnifiedUpgradeDialogStep.PlanSelection,
|
||||
selectedPlan: null,
|
||||
planSelectionStepTitleOverride: "upgradeYourPlan",
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [NoopAnimationsModule, UnifiedUpgradeDialogComponent],
|
||||
providers: [
|
||||
{ provide: DialogRef, useValue: mockDialogRef },
|
||||
{ provide: DIALOG_DATA, useValue: customDialogData },
|
||||
],
|
||||
})
|
||||
.overrideComponent(UnifiedUpgradeDialogComponent, {
|
||||
remove: {
|
||||
imports: [UpgradeAccountComponent, UpgradePaymentComponent],
|
||||
},
|
||||
add: {
|
||||
imports: [MockUpgradeAccountComponent, MockUpgradePaymentComponent],
|
||||
},
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
const customFixture = TestBed.createComponent(UnifiedUpgradeDialogComponent);
|
||||
const customComponent = customFixture.componentInstance;
|
||||
customFixture.detectChanges();
|
||||
|
||||
expect(customComponent["planSelectionStepTitleOverride"]()).toBe("upgradeYourPlan");
|
||||
});
|
||||
});
|
||||
|
||||
describe("onPlanSelected", () => {
|
||||
it("should set selected plan and move to payment step", () => {
|
||||
component["onPlanSelected"](PersonalSubscriptionPricingTierIds.Premium);
|
||||
|
||||
expect(component["selectedPlan"]()).toBe(PersonalSubscriptionPricingTierIds.Premium);
|
||||
expect(component["step"]()).toBe(UnifiedUpgradeDialogStep.Payment);
|
||||
});
|
||||
});
|
||||
|
||||
describe("previousStep", () => {
|
||||
it("should go back to plan selection and clear selected plan", () => {
|
||||
component["step"].set(UnifiedUpgradeDialogStep.Payment);
|
||||
component["selectedPlan"].set(PersonalSubscriptionPricingTierIds.Premium);
|
||||
|
||||
component["previousStep"]();
|
||||
|
||||
expect(component["step"]()).toBe(UnifiedUpgradeDialogStep.PlanSelection);
|
||||
expect(component["selectedPlan"]()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("hideContinueWithoutUpgradingButton", () => {
|
||||
it("should default to false when not provided", () => {
|
||||
expect(component["hideContinueWithoutUpgradingButton"]()).toBe(false);
|
||||
});
|
||||
|
||||
it("should be set to true when provided in dialog config", async () => {
|
||||
TestBed.resetTestingModule();
|
||||
|
||||
const customDialogData: UnifiedUpgradeDialogParams = {
|
||||
account: mockAccount,
|
||||
initialStep: null,
|
||||
selectedPlan: null,
|
||||
hideContinueWithoutUpgradingButton: true,
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [NoopAnimationsModule, UnifiedUpgradeDialogComponent],
|
||||
providers: [
|
||||
{ provide: DialogRef, useValue: mockDialogRef },
|
||||
{ provide: DIALOG_DATA, useValue: customDialogData },
|
||||
],
|
||||
})
|
||||
.overrideComponent(UnifiedUpgradeDialogComponent, {
|
||||
remove: {
|
||||
imports: [UpgradeAccountComponent, UpgradePaymentComponent],
|
||||
},
|
||||
add: {
|
||||
imports: [MockUpgradeAccountComponent, MockUpgradePaymentComponent],
|
||||
},
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
const customFixture = TestBed.createComponent(UnifiedUpgradeDialogComponent);
|
||||
const customComponent = customFixture.componentInstance;
|
||||
customFixture.detectChanges();
|
||||
|
||||
expect(customComponent["hideContinueWithoutUpgradingButton"]()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Inject, OnInit, signal } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values";
|
||||
@@ -48,11 +49,17 @@ export type UnifiedUpgradeDialogResult = {
|
||||
* @property {Account} account - The user account information.
|
||||
* @property {UnifiedUpgradeDialogStep | null} [initialStep] - The initial step to show in the dialog, if any.
|
||||
* @property {PersonalSubscriptionPricingTierId | null} [selectedPlan] - Pre-selected subscription plan, if any.
|
||||
* @property {string | null} [dialogTitleMessageOverride] - Optional custom i18n key to override the default dialog title.
|
||||
* @property {boolean} [hideContinueWithoutUpgradingButton] - Whether to hide the "Continue without upgrading" button.
|
||||
* @property {boolean} [redirectOnCompletion] - Whether to redirect after successful upgrade. Premium upgrades redirect to subscription settings, Families upgrades redirect to organization vault.
|
||||
*/
|
||||
export type UnifiedUpgradeDialogParams = {
|
||||
account: Account;
|
||||
initialStep?: UnifiedUpgradeDialogStep | null;
|
||||
selectedPlan?: PersonalSubscriptionPricingTierId | null;
|
||||
planSelectionStepTitleOverride?: string | null;
|
||||
hideContinueWithoutUpgradingButton?: boolean;
|
||||
redirectOnCompletion?: boolean;
|
||||
};
|
||||
|
||||
@Component({
|
||||
@@ -73,6 +80,8 @@ export class UnifiedUpgradeDialogComponent implements OnInit {
|
||||
protected step = signal<UnifiedUpgradeDialogStep>(UnifiedUpgradeDialogStep.PlanSelection);
|
||||
protected selectedPlan = signal<PersonalSubscriptionPricingTierId | null>(null);
|
||||
protected account = signal<Account | null>(null);
|
||||
protected planSelectionStepTitleOverride = signal<string | null>(null);
|
||||
protected hideContinueWithoutUpgradingButton = signal<boolean>(false);
|
||||
|
||||
protected readonly PaymentStep = UnifiedUpgradeDialogStep.Payment;
|
||||
protected readonly PlanSelectionStep = UnifiedUpgradeDialogStep.PlanSelection;
|
||||
@@ -80,12 +89,17 @@ export class UnifiedUpgradeDialogComponent implements OnInit {
|
||||
constructor(
|
||||
private dialogRef: DialogRef<UnifiedUpgradeDialogResult>,
|
||||
@Inject(DIALOG_DATA) private params: UnifiedUpgradeDialogParams,
|
||||
private router: Router,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.account.set(this.params.account);
|
||||
this.step.set(this.params.initialStep ?? UnifiedUpgradeDialogStep.PlanSelection);
|
||||
this.selectedPlan.set(this.params.selectedPlan ?? null);
|
||||
this.planSelectionStepTitleOverride.set(this.params.planSelectionStepTitleOverride ?? null);
|
||||
this.hideContinueWithoutUpgradingButton.set(
|
||||
this.params.hideContinueWithoutUpgradingButton ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
protected onPlanSelected(planId: PersonalSubscriptionPricingTierId): void {
|
||||
@@ -132,7 +146,20 @@ export class UnifiedUpgradeDialogComponent implements OnInit {
|
||||
default:
|
||||
status = UnifiedUpgradeDialogStatus.Closed;
|
||||
}
|
||||
|
||||
this.close({ status, organizationId: result.organizationId });
|
||||
|
||||
if (
|
||||
this.params.redirectOnCompletion &&
|
||||
(status === UnifiedUpgradeDialogStatus.UpgradedToPremium ||
|
||||
status === UnifiedUpgradeDialogStatus.UpgradedToFamilies)
|
||||
) {
|
||||
const redirectUrl =
|
||||
status === UnifiedUpgradeDialogStatus.UpgradedToFamilies
|
||||
? `/organizations/${result.organizationId}/vault`
|
||||
: "/settings/subscription/user-subscription";
|
||||
void this.router.navigate([redirectUrl]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@if (!loading()) {
|
||||
<section
|
||||
class="tw-bg-background tw-rounded-xl tw-shadow-lg tw-max-w-5xl tw-min-w-[332px] tw-w-[870px] tw-border-secondary-100 tw-border-solid tw-border"
|
||||
class="tw-w-screen tw-max-h-screen tw-min-w-[332px] md:tw-max-w-4xl tw-overflow-y-auto tw-self-center tw-bg-background tw-rounded-xl tw-shadow-lg tw-border-secondary-100 tw-border-solid tw-border"
|
||||
cdkTrapFocus
|
||||
cdkTrapFocusAutoCapture
|
||||
>
|
||||
@@ -17,17 +17,17 @@
|
||||
<div class="tw-px-14 tw-pb-8">
|
||||
<div class="tw-flex tw-text-center tw-flex-col tw-pb-4">
|
||||
<h1 class="tw-font-semibold tw-text-[32px]">
|
||||
{{ "individualUpgradeWelcomeMessage" | i18n }}
|
||||
{{ dialogTitle() | i18n }}
|
||||
</h1>
|
||||
<p bitTypography="body1" class="tw-text-muted">
|
||||
{{ "individualUpgradeDescriptionMessage" | i18n }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="tw-flex tw-flex-row tw-gap-6 tw-mb-4">
|
||||
<div class="tw-grid tw-grid-cols-1 sm:tw-grid-cols-2 tw-gap-5 tw-mb-4">
|
||||
@if (premiumCardDetails) {
|
||||
<billing-pricing-card
|
||||
class="tw-flex-1 tw-basis-0 tw-min-w-0"
|
||||
class="tw-w-full tw-min-w-[216px] tw-max-w-[456px]"
|
||||
[tagline]="premiumCardDetails.tagline"
|
||||
[price]="premiumCardDetails.price"
|
||||
[button]="premiumCardDetails.button"
|
||||
@@ -42,7 +42,7 @@
|
||||
|
||||
@if (familiesCardDetails) {
|
||||
<billing-pricing-card
|
||||
class="tw-flex-1 tw-basis-0 tw-min-w-0"
|
||||
class="tw-w-full tw-min-w-[216px] tw-max-w-[456px]"
|
||||
[tagline]="familiesCardDetails.tagline"
|
||||
[price]="familiesCardDetails.price"
|
||||
[button]="familiesCardDetails.button"
|
||||
@@ -59,9 +59,11 @@
|
||||
<p bitTypography="helper" class="tw-text-muted tw-italic">
|
||||
{{ "individualUpgradeTaxInformationMessage" | i18n }}
|
||||
</p>
|
||||
<button bitLink linkType="primary" type="button" (click)="closeClicked.emit(closeStatus)">
|
||||
{{ "continueWithoutUpgrading" | i18n }}
|
||||
</button>
|
||||
@if (!hideContinueWithoutUpgradingButton()) {
|
||||
<button bitLink linkType="primary" type="button" (click)="closeClicked.emit(closeStatus)">
|
||||
{{ "continueWithoutUpgrading" | i18n }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -146,4 +146,46 @@ describe("UpgradeAccountComponent", () => {
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hideContinueWithoutUpgradingButton", () => {
|
||||
it("should show the continue without upgrading button by default", () => {
|
||||
const button = fixture.nativeElement.querySelector('button[bitLink][linkType="primary"]');
|
||||
expect(button).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should hide the continue without upgrading button when input is true", async () => {
|
||||
TestBed.resetTestingModule();
|
||||
|
||||
mockI18nService.t.mockImplementation((key) => key);
|
||||
mockSubscriptionPricingService.getPersonalSubscriptionPricingTiers$.mockReturnValue(
|
||||
of(mockPricingTiers),
|
||||
);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
NoopAnimationsModule,
|
||||
UpgradeAccountComponent,
|
||||
PricingCardComponent,
|
||||
CdkTrapFocus,
|
||||
],
|
||||
providers: [
|
||||
{ provide: I18nService, useValue: mockI18nService },
|
||||
{ provide: SubscriptionPricingService, useValue: mockSubscriptionPricingService },
|
||||
],
|
||||
})
|
||||
.overrideComponent(UpgradeAccountComponent, {
|
||||
remove: { imports: [BillingServicesModule] },
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
const customFixture = TestBed.createComponent(UpgradeAccountComponent);
|
||||
customFixture.componentRef.setInput("hideContinueWithoutUpgradingButton", true);
|
||||
customFixture.detectChanges();
|
||||
|
||||
const button = customFixture.nativeElement.querySelector(
|
||||
'button[bitLink][linkType="primary"]',
|
||||
);
|
||||
expect(button).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CdkTrapFocus } from "@angular/cdk/a11y";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, DestroyRef, OnInit, output, signal } from "@angular/core";
|
||||
import { Component, DestroyRef, OnInit, computed, input, output, signal } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -52,6 +52,8 @@ type CardDetails = {
|
||||
templateUrl: "./upgrade-account.component.html",
|
||||
})
|
||||
export class UpgradeAccountComponent implements OnInit {
|
||||
dialogTitleMessageOverride = input<string | null>(null);
|
||||
hideContinueWithoutUpgradingButton = input<boolean>(false);
|
||||
planSelected = output<PersonalSubscriptionPricingTierId>();
|
||||
closeClicked = output<UpgradeAccountStatus>();
|
||||
protected loading = signal(true);
|
||||
@@ -62,6 +64,10 @@ export class UpgradeAccountComponent implements OnInit {
|
||||
protected premiumPlanType = PersonalSubscriptionPricingTierIds.Premium;
|
||||
protected closeStatus = UpgradeAccountStatus.Closed;
|
||||
|
||||
protected dialogTitle = computed(() => {
|
||||
return this.dialogTitleMessageOverride() || "individualUpgradeWelcomeMessage";
|
||||
});
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private subscriptionPricingService: SubscriptionPricingService,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<div class="tw-px-2 tw-mt-3 tw-mb-2 tw-h-10">
|
||||
<div class="tw-rounded-full tw-bg-primary-100 tw-size-full">
|
||||
<!-- Note that this is a custom button style for premium upgrade because the style desired
|
||||
is not supported by the button in the CL. -->
|
||||
<button
|
||||
type="button"
|
||||
class="tw-py-1.5 tw-px-4 tw-flex tw-gap-2 tw-items-center tw-size-full focus-visible:tw-ring-2 focus-visible:tw-ring-offset-0 focus:tw-outline-none focus-visible:tw-outline-none focus-visible:tw-ring-text-alt2 focus-visible:tw-z-10 tw-font-semibold tw-rounded-full tw-transition tw-border tw-border-solid tw-text-left tw-bg-primary-100 tw-text-primary-600 tw-border-primary-600 hover:tw-bg-hover-default hover:tw-text-primary-700 hover:tw-border-primary-700"
|
||||
(click)="openUpgradeDialog()"
|
||||
>
|
||||
<i class="bwi bwi-premium" aria-hidden="true"></i>
|
||||
{{ "upgradeYourPlan" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user