mirror of
https://github.com/bitwarden/browser
synced 2026-02-01 01:03:39 +00:00
Merge branch 'main' into km/auto-kdf
This commit is contained in:
25
.claude/prompts/review-code.md
Normal file
25
.claude/prompts/review-code.md
Normal file
@@ -0,0 +1,25 @@
|
||||
Please review this pull request with a focus on:
|
||||
|
||||
- Code quality and best practices
|
||||
- Potential bugs or issues
|
||||
- Security implications
|
||||
- Performance considerations
|
||||
|
||||
Note: The PR branch is already checked out in the current working directory.
|
||||
|
||||
Provide a comprehensive review including:
|
||||
|
||||
- Summary of changes since last review
|
||||
- Critical issues found (be thorough)
|
||||
- Suggested improvements (be thorough)
|
||||
- Good practices observed (be concise - list only the most notable items without elaboration)
|
||||
- Action items for the author
|
||||
- Leverage collapsible <details> sections where appropriate for lengthy explanations or code snippets to enhance human readability
|
||||
|
||||
When reviewing subsequent commits:
|
||||
|
||||
- Track status of previously identified issues (fixed/unfixed/reopened)
|
||||
- Identify NEW problems introduced since last review
|
||||
- Note if fixes introduced new issues
|
||||
|
||||
IMPORTANT: Be comprehensive about issues and improvements. For good practices, be brief - just note what was done well without explaining why or praising excessively.
|
||||
8
.github/CODEOWNERS
vendored
8
.github/CODEOWNERS
vendored
@@ -30,7 +30,7 @@ libs/common/src/auth @bitwarden/team-auth-dev
|
||||
apps/browser/src/tools @bitwarden/team-tools-dev
|
||||
apps/cli/src/tools @bitwarden/team-tools-dev
|
||||
apps/desktop/src/app/tools @bitwarden/team-tools-dev
|
||||
apps/desktop/desktop_native/bitwarden_chromium_importer @bitwarden/team-tools-dev
|
||||
apps/desktop/desktop_native/chromium_importer @bitwarden/team-tools-dev
|
||||
apps/web/src/app/tools @bitwarden/team-tools-dev
|
||||
libs/angular/src/tools @bitwarden/team-tools-dev
|
||||
libs/common/src/models/export @bitwarden/team-tools-dev
|
||||
@@ -174,6 +174,7 @@ apps/desktop/src/key-management @bitwarden/team-key-management-dev
|
||||
apps/web/src/app/key-management @bitwarden/team-key-management-dev
|
||||
apps/browser/src/key-management @bitwarden/team-key-management-dev
|
||||
apps/cli/src/key-management @bitwarden/team-key-management-dev
|
||||
bitwarden_license/bit-web/src/app/key-management @bitwarden/team-key-management-dev
|
||||
libs/key-management @bitwarden/team-key-management-dev
|
||||
libs/key-management-ui @bitwarden/team-key-management-dev
|
||||
libs/common/src/key-management @bitwarden/team-key-management-dev
|
||||
@@ -223,3 +224,8 @@ apps/web/src/locales/en/messages.json
|
||||
**/jest.config.js @bitwarden/team-platform-dev
|
||||
**/project.jsons @bitwarden/team-platform-dev
|
||||
libs/pricing @bitwarden/team-billing-dev
|
||||
|
||||
# Claude related files
|
||||
.claude/ @bitwarden/team-ai-sme
|
||||
.github/workflows/respond.yml @bitwarden/team-ai-sme
|
||||
.github/workflows/review-code.yml @bitwarden/team-ai-sme
|
||||
|
||||
1
.github/renovate.json5
vendored
1
.github/renovate.json5
vendored
@@ -139,6 +139,7 @@
|
||||
"@babel/core",
|
||||
"@babel/preset-env",
|
||||
"@bitwarden/sdk-internal",
|
||||
"@bitwarden/commercial-sdk-internal",
|
||||
"@electron/fuses",
|
||||
"@electron/notarize",
|
||||
"@electron/rebuild",
|
||||
|
||||
14
.github/workflows/build-browser.yml
vendored
14
.github/workflows/build-browser.yml
vendored
@@ -219,12 +219,14 @@ jobs:
|
||||
archive_name_prefix: ""
|
||||
npm_command_prefix: "dist:"
|
||||
readable: "open source license"
|
||||
type: "oss"
|
||||
- build_prefix: "bit-"
|
||||
artifact_prefix: "bit-"
|
||||
source_archive_name_prefix: "bit-"
|
||||
archive_name_prefix: "bit-"
|
||||
npm_command_prefix: "dist:bit:"
|
||||
readable: "commercial license"
|
||||
type: "commercial"
|
||||
browser:
|
||||
- name: "chrome"
|
||||
npm_command_suffix: "chrome"
|
||||
@@ -279,6 +281,11 @@ jobs:
|
||||
run: npm ci
|
||||
working-directory: browser-source/
|
||||
|
||||
- name: Remove commercial packages
|
||||
if: ${{ matrix.license_type.type == 'oss' }}
|
||||
run: rm -rf node_modules/@bitwarden/commercial-sdk-internal
|
||||
working-directory: browser-source/
|
||||
|
||||
- name: Download SDK artifacts
|
||||
if: ${{ inputs.sdk_branch != '' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@main
|
||||
@@ -350,11 +357,13 @@ jobs:
|
||||
archive_name_prefix: ""
|
||||
npm_command_prefix: "dist:"
|
||||
readable: "open source license"
|
||||
type: "oss"
|
||||
- build_prefix: "bit-"
|
||||
artifact_prefix: "bit-"
|
||||
archive_name_prefix: "bit-"
|
||||
npm_command_prefix: "dist:bit:"
|
||||
readable: "commercial license"
|
||||
type: "commercial"
|
||||
env:
|
||||
_BUILD_NUMBER: ${{ needs.setup.outputs.adj_build_number }}
|
||||
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
|
||||
@@ -461,6 +470,11 @@ jobs:
|
||||
run: npm ci
|
||||
working-directory: ./
|
||||
|
||||
- name: Remove commercial packages
|
||||
if: ${{ matrix.license_type.type == 'oss' }}
|
||||
run: rm -rf node_modules/@bitwarden/commercial-sdk-internal
|
||||
working-directory: ./
|
||||
|
||||
- name: Download SDK Artifacts
|
||||
if: ${{ inputs.sdk_branch != '' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@main
|
||||
|
||||
18
.github/workflows/build-cli.yml
vendored
18
.github/workflows/build-cli.yml
vendored
@@ -98,8 +98,8 @@ jobs:
|
||||
]
|
||||
license_type:
|
||||
[
|
||||
{ build_prefix: "oss", artifact_prefix: "-oss", readable: "open source license" },
|
||||
{ build_prefix: "bit", artifact_prefix: "", readable: "commercial license" }
|
||||
{ type: "oss", build_prefix: "oss", artifact_prefix: "-oss", readable: "open source license" },
|
||||
{ type: "commercial", build_prefix: "bit", artifact_prefix: "", readable: "commercial license" }
|
||||
]
|
||||
runs-on: ${{ matrix.os.distro }}
|
||||
needs: setup
|
||||
@@ -140,6 +140,11 @@ jobs:
|
||||
run: npm ci
|
||||
working-directory: ./
|
||||
|
||||
- name: Remove commercial packages
|
||||
if: ${{ matrix.license_type.type == 'oss' }}
|
||||
run: rm -rf node_modules/@bitwarden/commercial-sdk-internal
|
||||
working-directory: ./
|
||||
|
||||
- name: Download SDK Artifacts
|
||||
if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@main
|
||||
@@ -291,8 +296,8 @@ jobs:
|
||||
matrix:
|
||||
license_type:
|
||||
[
|
||||
{ build_prefix: "oss", artifact_prefix: "-oss", readable: "open source license" },
|
||||
{ build_prefix: "bit", artifact_prefix: "", readable: "commercial license" }
|
||||
{ type: "oss", build_prefix: "oss", artifact_prefix: "-oss", readable: "open source license" },
|
||||
{ type: "commercial", build_prefix: "bit", artifact_prefix: "", readable: "commercial license" }
|
||||
]
|
||||
runs-on: windows-2022
|
||||
permissions:
|
||||
@@ -410,6 +415,11 @@ jobs:
|
||||
run: npm ci
|
||||
working-directory: ./
|
||||
|
||||
- name: Remove commercial packages
|
||||
if: ${{ matrix.license_type.type == 'oss' }}
|
||||
run: Remove-Item -Recurse -Force -ErrorAction SilentlyContinue "node_modules/@bitwarden/commercial-sdk-internal"
|
||||
working-directory: ./
|
||||
|
||||
- name: Download SDK Artifacts
|
||||
if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@main
|
||||
|
||||
10
.github/workflows/build-web.yml
vendored
10
.github/workflows/build-web.yml
vendored
@@ -99,34 +99,43 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- artifact_name: selfhosted-open-source
|
||||
license_type: "oss"
|
||||
image_name: web-oss
|
||||
npm_command: dist:oss:selfhost
|
||||
- artifact_name: cloud-COMMERCIAL
|
||||
license_type: "commercial"
|
||||
image_name: web-cloud
|
||||
npm_command: dist:bit:cloud
|
||||
- artifact_name: selfhosted-COMMERCIAL
|
||||
license_type: "commercial"
|
||||
image_name: web
|
||||
npm_command: dist:bit:selfhost
|
||||
- artifact_name: selfhosted-DEV
|
||||
license_type: "commercial"
|
||||
image_name: web
|
||||
npm_command: build:bit:selfhost:dev
|
||||
git_metadata: true
|
||||
- artifact_name: cloud-QA
|
||||
license_type: "commercial"
|
||||
image_name: web-qa-cloud
|
||||
npm_command: build:bit:qa
|
||||
git_metadata: true
|
||||
- artifact_name: ee
|
||||
license_type: "commercial"
|
||||
image_name: web-ee
|
||||
npm_command: build:bit:ee
|
||||
git_metadata: true
|
||||
- artifact_name: cloud-euprd
|
||||
license_type: "commercial"
|
||||
image_name: web-euprd
|
||||
npm_command: build:bit:euprd
|
||||
- artifact_name: cloud-euqa
|
||||
license_type: "commercial"
|
||||
image_name: web-euqa
|
||||
npm_command: build:bit:euqa
|
||||
git_metadata: true
|
||||
- artifact_name: cloud-usdev
|
||||
license_type: "commercial"
|
||||
image_name: web-usdev
|
||||
npm_command: build:bit:usdev
|
||||
git_metadata: true
|
||||
@@ -269,6 +278,7 @@ jobs:
|
||||
build-args: |
|
||||
NODE_VERSION=${{ env._NODE_VERSION }}
|
||||
NPM_COMMAND=${{ matrix.npm_command }}
|
||||
LICENSE_TYPE=${{ matrix.license_type }}
|
||||
context: .
|
||||
file: apps/web/Dockerfile
|
||||
load: true
|
||||
|
||||
12
.github/workflows/lint.yml
vendored
12
.github/workflows/lint.yml
vendored
@@ -75,6 +75,9 @@ jobs:
|
||||
- name: Lint unowned dependencies
|
||||
run: npm run lint:dep-ownership
|
||||
|
||||
- name: Lint sdk-internal versions
|
||||
run: npm run lint:sdk-internal-versions
|
||||
|
||||
- name: Run linter
|
||||
run: npm run lint
|
||||
|
||||
@@ -114,3 +117,12 @@ jobs:
|
||||
- name: Cargo sort
|
||||
working-directory: ./apps/desktop/desktop_native
|
||||
run: cargo sort --workspace --check
|
||||
|
||||
- name: Install cargo-deny
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-deny
|
||||
|
||||
- name: Run cargo deny
|
||||
working-directory: ./apps/desktop/desktop_native
|
||||
run: cargo deny --log-level error --all-features check all
|
||||
|
||||
28
.github/workflows/respond.yml
vendored
Normal file
28
.github/workflows/respond.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: Respond
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, assigned]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
respond:
|
||||
name: Respond
|
||||
uses: bitwarden/gh-actions/.github/workflows/_respond.yml@main
|
||||
secrets:
|
||||
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
permissions:
|
||||
actions: read
|
||||
contents: write
|
||||
id-token: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
118
.github/workflows/review-code.yml
vendored
118
.github/workflows/review-code.yml
vendored
@@ -1,124 +1,20 @@
|
||||
name: Review code
|
||||
name: Code Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
review:
|
||||
name: Review
|
||||
runs-on: ubuntu-24.04
|
||||
uses: bitwarden/gh-actions/.github/workflows/_review-code.yml@main
|
||||
secrets:
|
||||
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check for Vault team changes
|
||||
id: check_changes
|
||||
run: |
|
||||
# Ensure we have the base branch
|
||||
git fetch origin ${{ github.base_ref }}
|
||||
|
||||
echo "Comparing changes between origin/${{ github.base_ref }} and HEAD"
|
||||
CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD)
|
||||
|
||||
if [ -z "$CHANGED_FILES" ]; then
|
||||
echo "Zero files changed"
|
||||
echo "vault_team_changes=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Handle variations in spacing and multiple teams
|
||||
VAULT_PATTERNS=$(grep -E "@bitwarden/team-vault-dev(\s|$)" .github/CODEOWNERS 2>/dev/null | awk '{print $1}')
|
||||
|
||||
if [ -z "$VAULT_PATTERNS" ]; then
|
||||
echo "⚠️ No patterns found for @bitwarden/team-vault-dev in CODEOWNERS"
|
||||
echo "vault_team_changes=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
vault_team_changes=false
|
||||
for pattern in $VAULT_PATTERNS; do
|
||||
echo "Checking pattern: $pattern"
|
||||
|
||||
# Handle **/directory patterns
|
||||
if [[ "$pattern" == "**/"* ]]; then
|
||||
# Remove the **/ prefix
|
||||
dir_pattern="${pattern#\*\*/}"
|
||||
# Check if any file contains this directory in its path
|
||||
if echo "$CHANGED_FILES" | grep -qE "(^|/)${dir_pattern}(/|$)"; then
|
||||
vault_team_changes=true
|
||||
echo "✅ Found files matching pattern: $pattern"
|
||||
echo "$CHANGED_FILES" | grep -E "(^|/)${dir_pattern}(/|$)" | sed 's/^/ - /'
|
||||
break
|
||||
fi
|
||||
else
|
||||
# Handle other patterns (shouldn't happen based on your CODEOWNERS)
|
||||
if echo "$CHANGED_FILES" | grep -q "$pattern"; then
|
||||
vault_team_changes=true
|
||||
echo "✅ Found files matching pattern: $pattern"
|
||||
echo "$CHANGED_FILES" | grep "$pattern" | sed 's/^/ - /'
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
echo "vault_team_changes=$vault_team_changes" >> $GITHUB_OUTPUT
|
||||
|
||||
if [ "$vault_team_changes" = "true" ]; then
|
||||
echo ""
|
||||
echo "✅ Vault team changes detected - proceeding with review"
|
||||
else
|
||||
echo ""
|
||||
echo "❌ No Vault team changes detected - skipping review"
|
||||
fi
|
||||
|
||||
- name: Review with Claude Code
|
||||
if: steps.check_changes.outputs.vault_team_changes == 'true'
|
||||
uses: anthropics/claude-code-action@ac1a3207f3f00b4a37e2f3a6f0935733c7c64651 # v1.0.11
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
track_progress: true
|
||||
use_sticky_comment: true
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number }}
|
||||
TITLE: ${{ github.event.pull_request.title }}
|
||||
BODY: ${{ github.event.pull_request.body }}
|
||||
AUTHOR: ${{ github.event.pull_request.user.login }}
|
||||
COMMIT: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
Please review this pull request with a focus on:
|
||||
- Code quality and best practices
|
||||
- Potential bugs or issues
|
||||
- Security implications
|
||||
- Performance considerations
|
||||
|
||||
Note: The PR branch is already checked out in the current working directory.
|
||||
|
||||
Provide a comprehensive review including:
|
||||
- Summary of changes since last review
|
||||
- Critical issues found (be thorough)
|
||||
- Suggested improvements (be thorough)
|
||||
- Good practices observed (be concise - list only the most notable items without elaboration)
|
||||
- Action items for the author
|
||||
- Leverage collapsible <details> sections where appropriate for lengthy explanations or code snippets to enhance human readability
|
||||
|
||||
When reviewing subsequent commits:
|
||||
- Track status of previously identified issues (fixed/unfixed/reopened)
|
||||
- Identify NEW problems introduced since last review
|
||||
- Note if fixes introduced new issues
|
||||
|
||||
IMPORTANT: Be comprehensive about issues and improvements. For good practices, be brief - just note what was done well without explaining why or praising excessively.
|
||||
|
||||
claude_args: |
|
||||
--allowedTools "mcp__github_comment__update_claude_comment,mcp__github_inline_comment__create_inline_comment,Bash(gh pr diff:*),Bash(gh pr view:*)"
|
||||
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
|
||||
- name: Trigger test-all workflow in browser-interactions-testing
|
||||
if: steps.changed-files.outputs.monitored == 'true'
|
||||
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0
|
||||
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4.0.0
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
repository: "bitwarden/browser-interactions-testing"
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,7 +10,6 @@ Thumbs.db
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
.claude
|
||||
.serena
|
||||
|
||||
# Visual Studio Code
|
||||
|
||||
2
.npmrc
2
.npmrc
@@ -1,4 +1,4 @@
|
||||
save-exact=true
|
||||
# Increase available heap size to avoid running out of memory when compiling.
|
||||
# This applies to all npm scripts in this repository.
|
||||
node-options=--max-old-space-size=8192
|
||||
node-options=--max-old-space-size=8192
|
||||
|
||||
@@ -588,6 +588,9 @@
|
||||
"view": {
|
||||
"message": "View"
|
||||
},
|
||||
"viewAll": {
|
||||
"message": "View all"
|
||||
},
|
||||
"viewLogin": {
|
||||
"message": "View login"
|
||||
},
|
||||
@@ -1028,6 +1031,18 @@
|
||||
"editedItem": {
|
||||
"message": "Item saved"
|
||||
},
|
||||
"savedWebsite": {
|
||||
"message": "Saved website"
|
||||
},
|
||||
"savedWebsites": {
|
||||
"message": "Saved websites ( $COUNT$ )",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
"example": "3"
|
||||
}
|
||||
}
|
||||
},
|
||||
"deleteItemConfirmation": {
|
||||
"message": "Do you really want to send to the trash?"
|
||||
},
|
||||
@@ -1694,9 +1709,30 @@
|
||||
"turnOffAutofill": {
|
||||
"message": "Turn off autofill"
|
||||
},
|
||||
"confirmAutofill": {
|
||||
"message": "Confirm autofill"
|
||||
},
|
||||
"confirmAutofillDesc": {
|
||||
"message": "This site doesn't match your saved login details. Before you fill in your login credentials, make sure it's a trusted site."
|
||||
},
|
||||
"showInlineMenuLabel": {
|
||||
"message": "Show autofill suggestions on form fields"
|
||||
},
|
||||
"howDoesBitwardenProtectFromPhishing": {
|
||||
"message": "How does Bitwarden protect your data from phishing?"
|
||||
},
|
||||
"currentWebsite": {
|
||||
"message": "Current website"
|
||||
},
|
||||
"autofillAndAddWebsite": {
|
||||
"message": "Autofill and add this website"
|
||||
},
|
||||
"autofillWithoutAdding": {
|
||||
"message": "Autofill without adding"
|
||||
},
|
||||
"doNotAutofill": {
|
||||
"message": "Do not autofill"
|
||||
},
|
||||
"showInlineMenuIdentitiesLabel": {
|
||||
"message": "Display identities as suggestions"
|
||||
},
|
||||
@@ -3258,6 +3294,9 @@
|
||||
"decryptionError": {
|
||||
"message": "Decryption error"
|
||||
},
|
||||
"errorGettingAutoFillData": {
|
||||
"message": "Error getting autofill data"
|
||||
},
|
||||
"couldNotDecryptVaultItemsBelow": {
|
||||
"message": "Bitwarden could not decrypt the vault item(s) listed below."
|
||||
},
|
||||
@@ -4029,6 +4068,15 @@
|
||||
"message": "Autofill on page load set to use default setting.",
|
||||
"description": "Toast message for informing the user that autofill on page load has been set to the default setting."
|
||||
},
|
||||
"cannotAutofill": {
|
||||
"message": "Cannot autofill"
|
||||
},
|
||||
"cannotAutofillExactMatch": {
|
||||
"message": "Default matching is set to 'Exact Match'. The current website does not exactly match the saved login details for this item."
|
||||
},
|
||||
"okay": {
|
||||
"message": "Okay"
|
||||
},
|
||||
"toggleSideNavigation": {
|
||||
"message": "Toggle side navigation"
|
||||
},
|
||||
@@ -5739,5 +5787,11 @@
|
||||
"settingDisabledByPolicy": {
|
||||
"message": "This setting is disabled by your organization's policy.",
|
||||
"description": "This hint text is displayed when a user setting is disabled due to an organization policy."
|
||||
},
|
||||
"zipPostalCodeLabel": {
|
||||
"message": "ZIP / Postal code"
|
||||
},
|
||||
"cardNumberLabel": {
|
||||
"message": "Card number"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Full routes that auth owns in the extension
|
||||
export const AuthExtensionRoute = Object.freeze({
|
||||
AccountSecurity: "account-security",
|
||||
DeviceManagement: "device-management",
|
||||
AccountSwitcher: "account-switcher",
|
||||
} as const);
|
||||
|
||||
export type AuthExtensionRoute = (typeof AuthExtensionRoute)[keyof typeof AuthExtensionRoute];
|
||||
1
apps/browser/src/auth/popup/constants/index.ts
Normal file
1
apps/browser/src/auth/popup/constants/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./auth-extension-route.constant";
|
||||
@@ -5,55 +5,5 @@
|
||||
<title>Bitwarden</title>
|
||||
<meta charset="utf-8" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="notification-bar-outer-wrapper" class="outer-wrapper">
|
||||
<div class="logo-wrapper">
|
||||
<a href="https://vault.bitwarden.com" target="_blank" id="logo-link" rel="noreferrer">
|
||||
<img id="logo" alt="Bitwarden" />
|
||||
</a>
|
||||
</div>
|
||||
<div id="content"></div>
|
||||
<div class="notification-close">
|
||||
<button type="button" class="neutral" id="close-button">
|
||||
<svg id="close" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none">
|
||||
<path
|
||||
d="M14.431 13.57 8.865 8.173a.388.388 0 0 1 0-.559l5.498-5.33a.388.388 0 0 0-.005-.553.415.415 0 0 0-.572-.006l-5.498 5.33a.416.416 0 0 1-.577 0L2.196 1.72a.403.403 0 0 0-.29-.12.422.422 0 0 0-.292.115.395.395 0 0 0-.12.283.386.386 0 0 0 .125.28l5.515 5.338a.388.388 0 0 1 0 .559L1.56 13.568a.397.397 0 0 0-.12.28c0 .105.044.205.12.28a.416.416 0 0 0 .578-.001l5.574-5.395a.416.416 0 0 1 .577 0l5.567 5.395a.422.422 0 0 0 .582.005.398.398 0 0 0 .12-.282.387.387 0 0 0-.125-.281Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<template id="template-add">
|
||||
<div class="inner-wrapper">
|
||||
<div id="add-text" class="notification-body"></div>
|
||||
<div class="add-change-cipher-buttons notification-actions">
|
||||
<button type="button" id="never-save" class="link"></button>
|
||||
<select id="select-folder"></select>
|
||||
<button type="button" id="add-edit" class="secondary"></button>
|
||||
<button type="button" id="add-save" class="primary"></button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="template-change">
|
||||
<div class="inner-wrapper">
|
||||
<div id="change-text" class="notification-body"></div>
|
||||
<div class="add-change-cipher-buttons notification-actions">
|
||||
<button type="button" id="change-edit" class="secondary"></button>
|
||||
<button type="button" id="change-save" class="primary"></button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="template-unlock">
|
||||
<div class="inner-wrapper">
|
||||
<div id="unlock-text" class="notification-body"></div>
|
||||
<div class="notification-actions">
|
||||
<button type="button" id="unlock-vault" class="primary"></button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<body></body>
|
||||
</html>
|
||||
|
||||
@@ -1,304 +0,0 @@
|
||||
@import "../shared/styles/variables";
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
font-family: $font-family-sans-serif;
|
||||
|
||||
@include themify($themes) {
|
||||
color: themed("textColor");
|
||||
background-color: themed("backgroundColor");
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
font-size: $font-size-base;
|
||||
font-family: $font-family-sans-serif;
|
||||
}
|
||||
|
||||
.outer-wrapper {
|
||||
display: block;
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
min-height: 42px;
|
||||
border: 1px solid transparent;
|
||||
border-bottom: 2px solid transparent;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
|
||||
@include themify($themes) {
|
||||
border-color: themed("borderColor");
|
||||
border-bottom-color: themed("primaryColor");
|
||||
}
|
||||
|
||||
&.success-event {
|
||||
@include themify($themes) {
|
||||
border-bottom-color: themed("successColor");
|
||||
}
|
||||
}
|
||||
|
||||
&.error-event {
|
||||
@include themify($themes) {
|
||||
border-bottom-color: themed("errorColor");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inner-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: auto max-content;
|
||||
}
|
||||
|
||||
.outer-wrapper > *,
|
||||
.inner-wrapper > * {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
#logo {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.logo-wrapper {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#close-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-right: 10px;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
@include themify($themes) {
|
||||
border-color: rgba(themed("textColor"), 0.2);
|
||||
background-color: rgba(themed("textColor"), 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#close {
|
||||
display: block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
> path {
|
||||
@include themify($themes) {
|
||||
fill: themed("textColor");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notification-close {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
}
|
||||
|
||||
#content .inner-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
|
||||
.notification-body {
|
||||
width: 100%;
|
||||
padding: 4px 38px 24px 42px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.notification-actions {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: stretch;
|
||||
justify-content: flex-end;
|
||||
|
||||
#never-save {
|
||||
margin-right: auto;
|
||||
padding: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
#select-folder {
|
||||
width: 125px;
|
||||
margin-right: 6px;
|
||||
font-size: 12px;
|
||||
appearance: none;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center right 4px;
|
||||
background-size: 16px;
|
||||
|
||||
@include themify($themes) {
|
||||
color: themed("mutedTextColor");
|
||||
border-color: themed("mutedTextColor");
|
||||
}
|
||||
|
||||
&:not([disabled]) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.primary,
|
||||
.secondary {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
margin-right: 6px;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.primary {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
&.success-message,
|
||||
&.error-message {
|
||||
padding: 4px 36px 6px 42px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 4px 8px;
|
||||
border-radius: $border-radius;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button.primary:not(.neutral) {
|
||||
@include themify($themes) {
|
||||
background-color: themed("primaryColor");
|
||||
color: themed("textContrast");
|
||||
border-color: themed("primaryColor");
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@include themify($themes) {
|
||||
background-color: darken(themed("primaryColor"), 1.5%);
|
||||
color: darken(themed("textContrast"), 6%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button.secondary:not(.neutral) {
|
||||
@include themify($themes) {
|
||||
background-color: themed("backgroundColor");
|
||||
color: themed("mutedTextColor");
|
||||
border-color: themed("mutedTextColor");
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@include themify($themes) {
|
||||
background-color: themed("backgroundOffsetColor");
|
||||
color: darken(themed("mutedTextColor"), 6%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button.link,
|
||||
button.neutral {
|
||||
@include themify($themes) {
|
||||
background-color: transparent;
|
||||
color: themed("primaryColor");
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
|
||||
@include themify($themes) {
|
||||
color: darken(themed("primaryColor"), 6%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
padding: 4px 6px;
|
||||
border: 1px solid #000000;
|
||||
border-radius: $border-radius;
|
||||
|
||||
@include themify($themes) {
|
||||
color: themed("textColor");
|
||||
background-color: themed("inputBackgroundColor");
|
||||
border-color: themed("inputBorderColor");
|
||||
}
|
||||
}
|
||||
|
||||
.success-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@include themify($themes) {
|
||||
color: themed("successColor");
|
||||
}
|
||||
|
||||
svg {
|
||||
margin-right: 8px;
|
||||
|
||||
path {
|
||||
@include themify($themes) {
|
||||
fill: themed("successColor");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-message {
|
||||
@include themify($themes) {
|
||||
color: themed("errorColor");
|
||||
}
|
||||
}
|
||||
|
||||
.success-event,
|
||||
.error-event {
|
||||
.notification-body {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
#select-folder {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
body {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.theme_light {
|
||||
#content .inner-wrapper {
|
||||
#select-folder {
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHhtbG5zOnhsaW5rPSdodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rJyB3aWR0aD0nMTYnIGhlaWdodD0nMTYnIGZpbGw9J25vbmUnPjxwYXRoIHN0cm9rZT0nIzIxMjUyOScgZD0nbTUgNiAzIDMgMy0zJy8+PC9zdmc+");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.theme_dark {
|
||||
#content .inner-wrapper {
|
||||
#select-folder {
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScxNicgaGVpZ2h0PScxNicgZmlsbD0nbm9uZSc+PHBhdGggc3Ryb2tlPScjZmZmZmZmJyBkPSdtNSA2IDMgMyAzLTMnLz48L3N2Zz4=");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,8 +187,6 @@ async function initNotificationBar(message: NotificationBarWindowMessage) {
|
||||
const notificationTestId = getNotificationTestId(notificationType);
|
||||
appendHeaderMessageToTitle(headerMessage);
|
||||
|
||||
document.body.innerHTML = "";
|
||||
|
||||
if (isVaultLocked) {
|
||||
const notificationConfig = {
|
||||
...notificationBarIframeInitData,
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { AutofillOverlayElement } from "../../../../enums/autofill-overlay.enum";
|
||||
|
||||
import { AutofillInlineMenuButton } from "./autofill-inline-menu-button";
|
||||
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
require("./button.scss");
|
||||
import "./button.css";
|
||||
|
||||
(function () {
|
||||
globalThis.customElements.define(AutofillOverlayElement.Button, AutofillInlineMenuButton);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
@import "../../../../shared/styles/variables";
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -27,10 +25,10 @@ autofill-inline-menu-button {
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
|
||||
.inline-menu-button-svg-icon {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.inline-menu-button .inline-menu-button-svg-icon {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`OverlayNotificationsContentService opening the notification bar creates the notification bar elements and appends them to the body 1`] = `
|
||||
exports[`OverlayNotificationsContentService opening the notification bar creates the notification bar elements and appends them to the body within a shadow root 1`] = `
|
||||
<div
|
||||
id="bit-notification-bar"
|
||||
style="height: 400px !important; width: 430px !important; max-width: calc(100% - 20px) !important; min-height: initial !important; top: 10px !important; right: 0px !important; padding: 0px !important; position: fixed !important; z-index: 2147483647 !important; visibility: visible !important; border-radius: 4px !important; background-color: transparent !important; overflow: hidden !important; transition: box-shadow 0.15s ease !important; transition-delay: 0.15s;"
|
||||
|
||||
@@ -16,10 +16,13 @@ describe("OverlayNotificationsContentService", () => {
|
||||
let domElementVisibilityService: DomElementVisibilityService;
|
||||
let autofillInit: AutofillInit;
|
||||
let bodyAppendChildSpy: jest.SpyInstance;
|
||||
let postMessageSpy: jest.SpyInstance<void, Parameters<Window["postMessage"]>>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.spyOn(utils, "sendExtensionMessage").mockImplementation(jest.fn());
|
||||
jest.spyOn(HTMLIFrameElement.prototype, "contentWindow", "get").mockReturnValue(window);
|
||||
postMessageSpy = jest.spyOn(window, "postMessage").mockImplementation(jest.fn());
|
||||
domQueryService = mock<DomQueryService>();
|
||||
domElementVisibilityService = new DomElementVisibilityService();
|
||||
overlayNotificationsContentService = new OverlayNotificationsContentService();
|
||||
@@ -48,7 +51,7 @@ describe("OverlayNotificationsContentService", () => {
|
||||
});
|
||||
|
||||
it("closes the notification bar if the notification bar type has changed", async () => {
|
||||
overlayNotificationsContentService["currentNotificationBarType"] = "add";
|
||||
overlayNotificationsContentService["currentNotificationBarType"] = NotificationType.AddLogin;
|
||||
const closeNotificationBarSpy = jest.spyOn(
|
||||
overlayNotificationsContentService as any,
|
||||
"closeNotificationBar",
|
||||
@@ -66,7 +69,7 @@ describe("OverlayNotificationsContentService", () => {
|
||||
expect(closeNotificationBarSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("creates the notification bar elements and appends them to the body", async () => {
|
||||
it("creates the notification bar elements and appends them to the body within a shadow root", async () => {
|
||||
sendMockExtensionMessage({
|
||||
command: "openNotificationBar",
|
||||
data: {
|
||||
@@ -77,6 +80,13 @@ describe("OverlayNotificationsContentService", () => {
|
||||
await flushPromises();
|
||||
|
||||
expect(overlayNotificationsContentService["notificationBarElement"]).toMatchSnapshot();
|
||||
|
||||
const rootElement = overlayNotificationsContentService["notificationBarRootElement"];
|
||||
expect(bodyAppendChildSpy).toHaveBeenCalledWith(rootElement);
|
||||
expect(rootElement?.tagName).toBe("BIT-NOTIFICATION-BAR-ROOT");
|
||||
|
||||
expect(document.getElementById("bit-notification-bar")).toBeNull();
|
||||
expect(document.querySelector("#bit-notification-bar-iframe")).toBeNull();
|
||||
});
|
||||
|
||||
it("sets up a slide in animation when the notification is fresh", async () => {
|
||||
@@ -116,6 +126,8 @@ describe("OverlayNotificationsContentService", () => {
|
||||
});
|
||||
|
||||
it("sends an initialization message to the notification bar iframe", async () => {
|
||||
const addEventListenerSpy = jest.spyOn(globalThis, "addEventListener");
|
||||
|
||||
sendMockExtensionMessage({
|
||||
command: "openNotificationBar",
|
||||
data: {
|
||||
@@ -124,10 +136,7 @@ describe("OverlayNotificationsContentService", () => {
|
||||
},
|
||||
});
|
||||
await flushPromises();
|
||||
const postMessageSpy = jest.spyOn(
|
||||
overlayNotificationsContentService["notificationBarIframeElement"].contentWindow,
|
||||
"postMessage",
|
||||
);
|
||||
expect(addEventListenerSpy).toHaveBeenCalledWith("message", expect.any(Function));
|
||||
|
||||
globalThis.dispatchEvent(
|
||||
new MessageEvent("message", {
|
||||
@@ -142,7 +151,6 @@ describe("OverlayNotificationsContentService", () => {
|
||||
);
|
||||
await flushPromises();
|
||||
|
||||
expect(postMessageSpy).toHaveBeenCalledTimes(1);
|
||||
expect(postMessageSpy).toHaveBeenCalledWith(
|
||||
{
|
||||
command: "initNotificationBar",
|
||||
@@ -158,7 +166,7 @@ describe("OverlayNotificationsContentService", () => {
|
||||
sendMockExtensionMessage({
|
||||
command: "openNotificationBar",
|
||||
data: {
|
||||
type: "change",
|
||||
type: NotificationType.ChangePassword,
|
||||
typeData: mock<NotificationTypeData>(),
|
||||
},
|
||||
});
|
||||
@@ -242,20 +250,15 @@ describe("OverlayNotificationsContentService", () => {
|
||||
});
|
||||
|
||||
it("sends a message to the notification bar iframe indicating that the save attempt completed", () => {
|
||||
jest.spyOn(
|
||||
overlayNotificationsContentService["notificationBarIframeElement"].contentWindow,
|
||||
"postMessage",
|
||||
);
|
||||
|
||||
sendMockExtensionMessage({
|
||||
command: "saveCipherAttemptCompleted",
|
||||
data: { error: undefined },
|
||||
});
|
||||
|
||||
expect(
|
||||
overlayNotificationsContentService["notificationBarIframeElement"].contentWindow
|
||||
.postMessage,
|
||||
).toHaveBeenCalledWith({ command: "saveCipherAttemptCompleted", error: undefined }, "*");
|
||||
expect(postMessageSpy).toHaveBeenCalledWith(
|
||||
{ command: "saveCipherAttemptCompleted", error: undefined },
|
||||
"*",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -271,9 +274,10 @@ describe("OverlayNotificationsContentService", () => {
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("triggers a closure of the notification bar", () => {
|
||||
it("triggers a closure of the notification bar and cleans up all shadow DOM elements", () => {
|
||||
overlayNotificationsContentService.destroy();
|
||||
|
||||
expect(overlayNotificationsContentService["notificationBarRootElement"]).toBeNull();
|
||||
expect(overlayNotificationsContentService["notificationBarElement"]).toBeNull();
|
||||
expect(overlayNotificationsContentService["notificationBarIframeElement"]).toBeNull();
|
||||
});
|
||||
|
||||
@@ -17,8 +17,10 @@ import {
|
||||
export class OverlayNotificationsContentService
|
||||
implements OverlayNotificationsContentServiceInterface
|
||||
{
|
||||
private notificationBarRootElement: HTMLElement | null = null;
|
||||
private notificationBarElement: HTMLElement | null = null;
|
||||
private notificationBarIframeElement: HTMLIFrameElement | null = null;
|
||||
private notificationBarShadowRoot: ShadowRoot | null = null;
|
||||
private currentNotificationBarType: NotificationType | null = null;
|
||||
private notificationBarContainerStyles: Partial<CSSStyleDeclaration> = {
|
||||
height: "400px",
|
||||
@@ -158,12 +160,12 @@ export class OverlayNotificationsContentService
|
||||
* @private
|
||||
*/
|
||||
private openNotificationBar(initData: NotificationBarIframeInitData) {
|
||||
if (!this.notificationBarElement && !this.notificationBarIframeElement) {
|
||||
if (!this.notificationBarRootElement && !this.notificationBarIframeElement) {
|
||||
this.createNotificationBarIframeElement(initData);
|
||||
this.createNotificationBarElement();
|
||||
|
||||
this.setupInitNotificationBarMessageListener(initData);
|
||||
globalThis.document.body.appendChild(this.notificationBarElement);
|
||||
globalThis.document.body.appendChild(this.notificationBarRootElement);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,15 +215,25 @@ export class OverlayNotificationsContentService
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates the container for the notification bar iframe.
|
||||
* Creates the container for the notification bar iframe with shadow DOM.
|
||||
*/
|
||||
private createNotificationBarElement() {
|
||||
if (this.notificationBarIframeElement) {
|
||||
this.notificationBarRootElement = globalThis.document.createElement(
|
||||
"bit-notification-bar-root",
|
||||
);
|
||||
|
||||
this.notificationBarShadowRoot = this.notificationBarRootElement.attachShadow({
|
||||
mode: "closed",
|
||||
delegatesFocus: true,
|
||||
});
|
||||
|
||||
this.notificationBarElement = globalThis.document.createElement("div");
|
||||
this.notificationBarElement.id = "bit-notification-bar";
|
||||
|
||||
setElementStyles(this.notificationBarElement, this.notificationBarContainerStyles, true);
|
||||
|
||||
this.notificationBarShadowRoot.appendChild(this.notificationBarElement);
|
||||
this.notificationBarElement.appendChild(this.notificationBarIframeElement);
|
||||
}
|
||||
}
|
||||
@@ -258,7 +270,7 @@ export class OverlayNotificationsContentService
|
||||
* @param closedByUserAction - Whether the notification bar was closed by the user.
|
||||
*/
|
||||
private closeNotificationBar(closedByUserAction: boolean = false) {
|
||||
if (!this.notificationBarElement && !this.notificationBarIframeElement) {
|
||||
if (!this.notificationBarRootElement && !this.notificationBarIframeElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -267,6 +279,9 @@ export class OverlayNotificationsContentService
|
||||
|
||||
this.notificationBarElement.remove();
|
||||
this.notificationBarElement = null;
|
||||
this.notificationBarShadowRoot = null;
|
||||
this.notificationBarRootElement.remove();
|
||||
this.notificationBarRootElement = null;
|
||||
|
||||
const removableNotificationTypes = new Set([
|
||||
NotificationTypes.Add,
|
||||
|
||||
@@ -26,7 +26,6 @@ const eventsToTest = [
|
||||
EVENTS.CHANGE,
|
||||
EVENTS.INPUT,
|
||||
EVENTS.KEYDOWN,
|
||||
EVENTS.KEYPRESS,
|
||||
EVENTS.KEYUP,
|
||||
"blur",
|
||||
"click",
|
||||
@@ -1044,13 +1043,13 @@ describe("InsertAutofillContentService", () => {
|
||||
});
|
||||
|
||||
describe("simulateUserKeyboardEventInteractions", () => {
|
||||
it("will trigger `keydown`, `keypress`, and `keyup` events on the passed element", () => {
|
||||
it("will trigger `keydown` and `keyup` events on the passed element", () => {
|
||||
const inputElement = document.querySelector('input[type="text"]') as HTMLInputElement;
|
||||
jest.spyOn(inputElement, "dispatchEvent");
|
||||
|
||||
insertAutofillContentService["simulateUserKeyboardEventInteractions"](inputElement);
|
||||
|
||||
[EVENTS.KEYDOWN, EVENTS.KEYPRESS, EVENTS.KEYUP].forEach((eventName) => {
|
||||
[EVENTS.KEYDOWN, EVENTS.KEYUP].forEach((eventName) => {
|
||||
expect(inputElement.dispatchEvent).toHaveBeenCalledWith(
|
||||
new KeyboardEvent(eventName, { bubbles: true }),
|
||||
);
|
||||
|
||||
@@ -136,7 +136,7 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
|
||||
setTimeout(() => {
|
||||
this.autofillInsertActions[action]({ opid, value });
|
||||
resolve();
|
||||
}, delayActionsInMilliseconds * actionIndex),
|
||||
}, delayActionsInMilliseconds),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -349,7 +349,7 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
|
||||
* @private
|
||||
*/
|
||||
private simulateUserKeyboardEventInteractions(element: FormFieldElement): void {
|
||||
const simulatedKeyboardEvents = [EVENTS.KEYDOWN, EVENTS.KEYPRESS, EVENTS.KEYUP];
|
||||
const simulatedKeyboardEvents = [EVENTS.KEYDOWN, EVENTS.KEYUP];
|
||||
for (let index = 0; index < simulatedKeyboardEvents.length; index++) {
|
||||
element.dispatchEvent(new KeyboardEvent(simulatedKeyboardEvents[index], { bubbles: true }));
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
|
||||
import { PhishingDetectionService } from "../services/phishing-detection.service";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "dirt-phishing-warning",
|
||||
standalone: true,
|
||||
|
||||
@@ -6,6 +6,8 @@ import { Component } from "@angular/core";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { ButtonModule, LinkModule } from "@bitwarden/components";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "dirt-phishing-protected-by",
|
||||
standalone: true,
|
||||
|
||||
@@ -7,12 +7,16 @@ import { IconButtonModule } from "@bitwarden/components";
|
||||
|
||||
import BrowserPopupUtils from "../../browser/browser-popup-utils";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-pop-out",
|
||||
templateUrl: "pop-out.component.html",
|
||||
imports: [CommonModule, JslibModule, IconButtonModule],
|
||||
})
|
||||
export class PopOutComponent implements OnInit {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() show = true;
|
||||
|
||||
constructor(private platformUtilsService: PlatformUtilsService) {}
|
||||
|
||||
@@ -15,23 +15,9 @@ export class BrowserFileDownloadService implements FileDownloadService {
|
||||
download(request: FileDownloadRequest): void {
|
||||
const builder = new FileDownloadBuilder(request);
|
||||
if (BrowserApi.isSafariApi) {
|
||||
let data: BlobPart = null;
|
||||
if (builder.blobOptions.type === "text/plain" && typeof request.blobData === "string") {
|
||||
data = request.blobData;
|
||||
} else {
|
||||
data = Utils.fromBufferToB64(request.blobData as ArrayBuffer);
|
||||
}
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
SafariApp.sendMessageToApp(
|
||||
"downloadFile",
|
||||
JSON.stringify({
|
||||
blobData: data,
|
||||
blobOptions: request.blobOptions,
|
||||
fileName: request.fileName,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
// Handle Safari download asynchronously to allow Blob conversion
|
||||
// This function can't be async because the interface is not async
|
||||
void this.downloadSafari(request, builder);
|
||||
} else {
|
||||
const a = window.document.createElement("a");
|
||||
a.href = URL.createObjectURL(builder.blob);
|
||||
@@ -41,4 +27,31 @@ export class BrowserFileDownloadService implements FileDownloadService {
|
||||
window.document.body.removeChild(a);
|
||||
}
|
||||
}
|
||||
|
||||
private async downloadSafari(
|
||||
request: FileDownloadRequest,
|
||||
builder: FileDownloadBuilder,
|
||||
): Promise<void> {
|
||||
let data: string = null;
|
||||
if (builder.blobOptions.type === "text/plain" && typeof request.blobData === "string") {
|
||||
data = request.blobData;
|
||||
} else if (request.blobData instanceof Blob) {
|
||||
// Convert Blob to ArrayBuffer first, then to Base64
|
||||
const arrayBuffer = await request.blobData.arrayBuffer();
|
||||
data = Utils.fromBufferToB64(arrayBuffer);
|
||||
} else {
|
||||
// Already an ArrayBuffer
|
||||
data = Utils.fromBufferToB64(request.blobData as ArrayBuffer);
|
||||
}
|
||||
|
||||
await SafariApp.sendMessageToApp(
|
||||
"downloadFile",
|
||||
JSON.stringify({
|
||||
blobData: data,
|
||||
blobOptions: request.blobOptions,
|
||||
fileName: request.fileName,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import { PopupRouterCacheService, popupRouterCacheGuard } from "./popup-router-c
|
||||
|
||||
const flushPromises = async () => await new Promise(process.nextTick);
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
template: "",
|
||||
standalone: false,
|
||||
|
||||
@@ -19,12 +19,16 @@ import {
|
||||
|
||||
import { PopupViewCacheService } from "./popup-view-cache.service";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
template: "",
|
||||
standalone: false,
|
||||
})
|
||||
export class EmptyComponent {}
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
template: "",
|
||||
standalone: false,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Injectable, NgModule } from "@angular/core";
|
||||
import { ActivatedRouteSnapshot, RouteReuseStrategy, RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { AuthenticationTimeoutComponent } from "@bitwarden/angular/auth/components/authentication-timeout.component";
|
||||
import { AuthRoute } from "@bitwarden/angular/auth/constants";
|
||||
import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/environment-selector/environment-selector.component";
|
||||
import {
|
||||
activeAuthGuard,
|
||||
@@ -45,6 +46,7 @@ import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "@bitwarden/co
|
||||
import { LockComponent, ConfirmKeyConnectorDomainComponent } from "@bitwarden/key-management-ui";
|
||||
|
||||
import { AccountSwitcherComponent } from "../auth/popup/account-switching/account-switcher.component";
|
||||
import { AuthExtensionRoute } from "../auth/popup/constants/auth-extension-route.constant";
|
||||
import { fido2AuthGuard } from "../auth/popup/guards/fido2-auth.guard";
|
||||
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
|
||||
import { ExtensionDeviceManagementComponent } from "../auth/popup/settings/extension-device-management.component";
|
||||
@@ -148,7 +150,7 @@ const routes: Routes = [
|
||||
component: ExtensionAnonLayoutWrapperComponent,
|
||||
children: [
|
||||
{
|
||||
path: "authentication-timeout",
|
||||
path: AuthRoute.AuthenticationTimeout,
|
||||
canActivate: [unauthGuardFn(unauthRouteOverrides)],
|
||||
children: [
|
||||
{
|
||||
@@ -167,7 +169,7 @@ const routes: Routes = [
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "device-verification",
|
||||
path: AuthRoute.NewDeviceVerification,
|
||||
component: ExtensionAnonLayoutWrapperComponent,
|
||||
canActivate: [unauthGuardFn(), activeAuthGuard()],
|
||||
children: [{ path: "", component: NewDeviceVerificationComponent }],
|
||||
@@ -259,13 +261,13 @@ const routes: Routes = [
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "account-security",
|
||||
path: AuthExtensionRoute.AccountSecurity,
|
||||
component: AccountSecurityComponent,
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "device-management",
|
||||
path: AuthExtensionRoute.DeviceManagement,
|
||||
component: ExtensionDeviceManagementComponent,
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
@@ -341,7 +343,7 @@ const routes: Routes = [
|
||||
component: ExtensionAnonLayoutWrapperComponent,
|
||||
children: [
|
||||
{
|
||||
path: "signup",
|
||||
path: AuthRoute.SignUp,
|
||||
canActivate: [unauthGuardFn()],
|
||||
data: {
|
||||
elevation: 1,
|
||||
@@ -361,13 +363,13 @@ const routes: Routes = [
|
||||
component: RegistrationStartSecondaryComponent,
|
||||
outlet: "secondary",
|
||||
data: {
|
||||
loginRoute: "/login",
|
||||
loginRoute: `/${AuthRoute.Login}`,
|
||||
} satisfies RegistrationStartSecondaryComponentData,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "finish-signup",
|
||||
path: AuthRoute.FinishSignUp,
|
||||
canActivate: [unauthGuardFn()],
|
||||
data: {
|
||||
pageIcon: LockIcon,
|
||||
@@ -382,7 +384,7 @@ const routes: Routes = [
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "set-initial-password",
|
||||
path: AuthRoute.SetInitialPassword,
|
||||
canActivate: [authGuard],
|
||||
component: SetInitialPasswordComponent,
|
||||
data: {
|
||||
@@ -390,7 +392,7 @@ const routes: Routes = [
|
||||
} satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "login",
|
||||
path: AuthRoute.Login,
|
||||
canActivate: [unauthGuardFn(unauthRouteOverrides), IntroCarouselGuard],
|
||||
data: {
|
||||
pageIcon: VaultIcon,
|
||||
@@ -411,7 +413,7 @@ const routes: Routes = [
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "login-with-passkey",
|
||||
path: AuthRoute.LoginWithPasskey,
|
||||
canActivate: [unauthGuardFn(unauthRouteOverrides)],
|
||||
data: {
|
||||
pageIcon: TwoFactorAuthSecurityKeyIcon,
|
||||
@@ -434,7 +436,7 @@ const routes: Routes = [
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "sso",
|
||||
path: AuthRoute.Sso,
|
||||
canActivate: [unauthGuardFn(unauthRouteOverrides)],
|
||||
data: {
|
||||
pageIcon: VaultIcon,
|
||||
@@ -456,7 +458,7 @@ const routes: Routes = [
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "login-with-device",
|
||||
path: AuthRoute.LoginWithDevice,
|
||||
canActivate: [redirectToVaultIfUnlockedGuard()],
|
||||
data: {
|
||||
pageIcon: DevicesIcon,
|
||||
@@ -479,7 +481,7 @@ const routes: Routes = [
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "hint",
|
||||
path: AuthRoute.PasswordHint,
|
||||
canActivate: [unauthGuardFn(unauthRouteOverrides)],
|
||||
data: {
|
||||
pageTitle: {
|
||||
@@ -502,7 +504,7 @@ const routes: Routes = [
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "admin-approval-requested",
|
||||
path: AuthRoute.AdminApprovalRequested,
|
||||
canActivate: [redirectToVaultIfUnlockedGuard()],
|
||||
data: {
|
||||
pageIcon: DevicesIcon,
|
||||
@@ -519,7 +521,7 @@ const routes: Routes = [
|
||||
children: [{ path: "", component: LoginViaAuthRequestComponent }],
|
||||
},
|
||||
{
|
||||
path: "login-initiated",
|
||||
path: AuthRoute.LoginInitiated,
|
||||
canActivate: [tdeDecryptionRequiredGuard()],
|
||||
data: {
|
||||
pageIcon: DevicesIcon,
|
||||
@@ -557,7 +559,7 @@ const routes: Routes = [
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "2fa",
|
||||
path: AuthRoute.TwoFactor,
|
||||
canActivate: [unauthGuardFn(unauthRouteOverrides), TwoFactorAuthGuard],
|
||||
children: [
|
||||
{
|
||||
@@ -576,7 +578,7 @@ const routes: Routes = [
|
||||
} satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData,
|
||||
},
|
||||
{
|
||||
path: "change-password",
|
||||
path: AuthRoute.ChangePassword,
|
||||
data: {
|
||||
elevation: 1,
|
||||
hideFooter: true,
|
||||
@@ -698,7 +700,7 @@ const routes: Routes = [
|
||||
canActivate: [authGuard, canAccessAtRiskPasswords, hasAtRiskPasswords],
|
||||
},
|
||||
{
|
||||
path: "account-switcher",
|
||||
path: AuthExtensionRoute.AccountSwitcher,
|
||||
component: AccountSwitcherComponent,
|
||||
data: { elevation: 4, doNotSaveUrl: true } satisfies RouteDataProperties,
|
||||
},
|
||||
|
||||
@@ -67,6 +67,8 @@ import { initPopupClosedListener } from "../platform/services/popup-view-cache-b
|
||||
import { routerTransition } from "./app-routing.animations";
|
||||
import { DesktopSyncVerificationDialogComponent } from "./components/desktop-sync-verification-dialog.component";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-root",
|
||||
styles: [],
|
||||
|
||||
@@ -15,6 +15,8 @@ export type DesktopSyncVerificationDialogParams = {
|
||||
fingerprint: string[];
|
||||
};
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
templateUrl: "desktop-sync-verification-dialog.component.html",
|
||||
imports: [JslibModule, ButtonModule, DialogModule],
|
||||
|
||||
@@ -17,6 +17,8 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
|
||||
import { NavButton } from "../platform/popup/layout/popup-tab-navigation.component";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-tabs-v2",
|
||||
templateUrl: "./tabs-v2.component.html",
|
||||
|
||||
@@ -69,8 +69,8 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
|
||||
if let url = panel.url {
|
||||
do {
|
||||
let fileManager = FileManager.default
|
||||
if !fileManager.fileExists(atPath: url.absoluteString) {
|
||||
fileManager.createFile(atPath: url.absoluteString, contents: Data(),
|
||||
if !fileManager.fileExists(atPath: url.path) {
|
||||
fileManager.createFile(atPath: url.path, contents: Data(),
|
||||
attributes: nil)
|
||||
}
|
||||
try data.write(to: url)
|
||||
|
||||
@@ -10,6 +10,8 @@ import { AnchorLinkDirective, CalloutModule, BannerModule } from "@bitwarden/com
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import { AtRiskPasswordCalloutData, AtRiskPasswordCalloutService } from "@bitwarden/vault";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "vault-at-risk-password-callout",
|
||||
imports: [
|
||||
|
||||
@@ -17,6 +17,8 @@ export const AtRiskCarouselDialogResult = {
|
||||
|
||||
type AtRiskCarouselDialogResult = UnionOfValues<typeof AtRiskCarouselDialogResult>;
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "vault-at-risk-carousel-dialog",
|
||||
templateUrl: "./at-risk-carousel-dialog.component.html",
|
||||
@@ -32,6 +34,8 @@ type AtRiskCarouselDialogResult = UnionOfValues<typeof AtRiskCarouselDialogResul
|
||||
export class AtRiskCarouselDialogComponent {
|
||||
private dialogRef = inject(DialogRef);
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
protected dismissBtnEnabled = signal(false);
|
||||
|
||||
protected async dismiss() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
import { ChangeDetectionStrategy, Component, input } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { mock } from "jest-mock-extended";
|
||||
@@ -40,26 +40,29 @@ import { AtRiskPasswordsComponent } from "./at-risk-passwords.component";
|
||||
@Component({
|
||||
selector: "popup-header",
|
||||
template: `<ng-content></ng-content>`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
class MockPopupHeaderComponent {
|
||||
@Input() pageTitle: string | undefined;
|
||||
@Input() backAction: (() => void) | undefined;
|
||||
readonly pageTitle = input<string | undefined>(undefined);
|
||||
readonly backAction = input<(() => void) | undefined>(undefined);
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "popup-page",
|
||||
template: `<ng-content></ng-content>`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
class MockPopupPageComponent {
|
||||
@Input() loading: boolean | undefined;
|
||||
readonly loading = input<boolean | undefined>(undefined);
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-icon",
|
||||
template: `<ng-content></ng-content>`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
class MockAppIcon {
|
||||
@Input() cipher: CipherView | undefined;
|
||||
readonly cipher = input<CipherView | undefined>(undefined);
|
||||
}
|
||||
|
||||
describe("AtRiskPasswordsComponent", () => {
|
||||
@@ -95,11 +98,15 @@ describe("AtRiskPasswordsComponent", () => {
|
||||
id: "cipher",
|
||||
organizationId: "org",
|
||||
name: "Item 1",
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
} as CipherView,
|
||||
{
|
||||
id: "cipher2",
|
||||
organizationId: "org",
|
||||
name: "Item 2",
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
} as CipherView,
|
||||
]);
|
||||
mockOrgs$ = new BehaviorSubject<Organization[]>([
|
||||
@@ -221,6 +228,38 @@ describe("AtRiskPasswordsComponent", () => {
|
||||
organizationId: "org",
|
||||
name: "Item 1",
|
||||
isDeleted: true,
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
} as CipherView,
|
||||
]);
|
||||
|
||||
const items = await firstValueFrom(component["atRiskItems$"]);
|
||||
expect(items).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should not show tasks when cipher does not have edit permission", async () => {
|
||||
mockCiphers$.next([
|
||||
{
|
||||
id: "cipher",
|
||||
organizationId: "org",
|
||||
name: "Item 1",
|
||||
edit: false,
|
||||
viewPassword: true,
|
||||
} as CipherView,
|
||||
]);
|
||||
|
||||
const items = await firstValueFrom(component["atRiskItems$"]);
|
||||
expect(items).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should not show tasks when cipher does not have viewPassword permission", async () => {
|
||||
mockCiphers$.next([
|
||||
{
|
||||
id: "cipher",
|
||||
organizationId: "org",
|
||||
name: "Item 1",
|
||||
edit: true,
|
||||
viewPassword: false,
|
||||
} as CipherView,
|
||||
]);
|
||||
|
||||
@@ -274,11 +313,15 @@ describe("AtRiskPasswordsComponent", () => {
|
||||
id: "cipher",
|
||||
organizationId: "org",
|
||||
name: "Item 1",
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
} as CipherView,
|
||||
{
|
||||
id: "cipher2",
|
||||
organizationId: "org2",
|
||||
name: "Item 2",
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
} as CipherView,
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, DestroyRef, inject, OnInit, signal } from "@angular/core";
|
||||
import {
|
||||
Component,
|
||||
DestroyRef,
|
||||
inject,
|
||||
OnInit,
|
||||
signal,
|
||||
ChangeDetectionStrategy,
|
||||
} from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { Router } from "@angular/router";
|
||||
import {
|
||||
@@ -80,6 +87,7 @@ import { AtRiskPasswordPageService } from "./at-risk-password-page.service";
|
||||
],
|
||||
selector: "vault-at-risk-passwords",
|
||||
templateUrl: "./at-risk-passwords.component.html",
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AtRiskPasswordsComponent implements OnInit {
|
||||
private taskService = inject(TaskService);
|
||||
@@ -156,6 +164,8 @@ export class AtRiskPasswordsComponent implements OnInit {
|
||||
t.type === SecurityTaskType.UpdateAtRiskCredential &&
|
||||
t.cipherId != null &&
|
||||
ciphers[t.cipherId] != null &&
|
||||
ciphers[t.cipherId].edit &&
|
||||
ciphers[t.cipherId].viewPassword &&
|
||||
!ciphers[t.cipherId].isDeleted,
|
||||
)
|
||||
.map((t) => ciphers[t.cipherId!]),
|
||||
|
||||
@@ -131,6 +131,8 @@ class QueryParams {
|
||||
|
||||
export type AddEditQueryParams = Partial<Record<keyof QueryParams, string>>;
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-add-edit-v2",
|
||||
templateUrl: "add-edit-v2.component.html",
|
||||
|
||||
@@ -28,6 +28,8 @@ import { PopupFooterComponent } from "../../../../../platform/popup/layout/popup
|
||||
import { PopupHeaderComponent } from "../../../../../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "../../../../../platform/popup/layout/popup-page.component";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-assign-collections",
|
||||
templateUrl: "./assign-collections.component.html",
|
||||
|
||||
@@ -25,20 +25,30 @@ import { PopupRouterCacheService } from "../../../../../platform/popup/view-cach
|
||||
|
||||
import { AttachmentsV2Component } from "./attachments-v2.component";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "popup-header",
|
||||
template: `<ng-content></ng-content>`,
|
||||
})
|
||||
class MockPopupHeaderComponent {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() pageTitle: string;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() backAction: () => void;
|
||||
}
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "popup-footer",
|
||||
template: `<ng-content></ng-content>`,
|
||||
})
|
||||
class MockPopupFooterComponent {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() pageTitle: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ import { PopupHeaderComponent } from "../../../../../platform/popup/layout/popup
|
||||
import { PopupPageComponent } from "../../../../../platform/popup/layout/popup-page.component";
|
||||
import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-attachments-v2",
|
||||
templateUrl: "./attachments-v2.component.html",
|
||||
|
||||
@@ -25,6 +25,8 @@ import { CipherFormContainer } from "@bitwarden/vault";
|
||||
import BrowserPopupUtils from "../../../../../../platform/browser/browser-popup-utils";
|
||||
import { FilePopoutUtilsService } from "../../../../../../tools/popup/services/file-popout-utils.service";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-open-attachments",
|
||||
templateUrl: "./open-attachments.component.html",
|
||||
@@ -39,6 +41,8 @@ import { FilePopoutUtilsService } from "../../../../../../tools/popup/services/f
|
||||
})
|
||||
export class OpenAttachmentsComponent implements OnInit {
|
||||
/** Cipher `id` */
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input({ required: true }) cipherId: CipherId;
|
||||
|
||||
/** True when the attachments window should be opened in a popout */
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<bit-dialog>
|
||||
<span bitDialogTitle>{{ "confirmAutofill" | i18n }}</span>
|
||||
<div bitDialogContent>
|
||||
<p bitTypography="body2">
|
||||
{{ "confirmAutofillDesc" | i18n }}
|
||||
</p>
|
||||
@if (savedUrls.length === 1) {
|
||||
<p class="tw-text-muted tw-text-xs tw-uppercase tw-mt-4 tw-font-semibold">
|
||||
{{ "savedWebsite" | i18n }}
|
||||
</p>
|
||||
<bit-callout [title]="null" type="success" icon="bwi-globe">
|
||||
<div class="tw-font-mono tw-line-clamp-1 tw-break-all" [appA11yTitle]="savedUrls[0]">
|
||||
{{ savedUrls[0] }}
|
||||
</div>
|
||||
</bit-callout>
|
||||
}
|
||||
@if (savedUrls.length > 1) {
|
||||
<div class="tw-flex tw-justify-between tw-items-center tw-mt-4 tw-mb-1 tw-pt-2">
|
||||
<p class="tw-text-muted tw-text-xs tw-uppercase tw-font-semibold">
|
||||
{{ "savedWebsites" | i18n: savedUrls.length }}
|
||||
</p>
|
||||
<button
|
||||
*ngIf="!savedUrlsExpanded"
|
||||
type="button"
|
||||
bitLink
|
||||
class="tw-text-sm tw-font-bold tw-cursor-pointer"
|
||||
(click)="viewAllSavedUrls()"
|
||||
>
|
||||
{{ "viewAll" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="tw-pt-2" [ngClass]="savedUrlsListClass">
|
||||
<div class="-tw-mt-2" *ngFor="let url of savedUrls">
|
||||
<bit-callout [title]="null" type="success" icon="bwi-globe">
|
||||
<div class="tw-font-mono tw-line-clamp-1 tw-break-all" [appA11yTitle]="url">
|
||||
{{ url }}
|
||||
</div>
|
||||
</bit-callout>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<p class="tw-text-muted tw-text-xs tw-uppercase tw-mt-5 tw-font-semibold">
|
||||
{{ "currentWebsite" | i18n }}
|
||||
</p>
|
||||
<bit-callout [title]="null" type="warning" icon="bwi-globe">
|
||||
<div [appA11yTitle]="currentUrl" class="tw-font-mono tw-line-clamp-1 tw-break-all">
|
||||
{{ currentUrl }}
|
||||
</div>
|
||||
</bit-callout>
|
||||
<div class="tw-flex tw-justify-center tw-flex-col tw-gap-3 tw-mt-6">
|
||||
<button type="button" bitButton buttonType="primary" (click)="autofillAndAddUrl()">
|
||||
{{ "autofillAndAddWebsite" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton buttonType="secondary" (click)="autofillOnly()">
|
||||
{{ "autofillWithoutAdding" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitLink
|
||||
linkType="secondary"
|
||||
(click)="close()"
|
||||
class="tw-mt-2 tw-font-bold tw-text-sm tw-justify-center tw-text-center"
|
||||
>
|
||||
{{ "doNotAutofill" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</bit-dialog>
|
||||
@@ -0,0 +1,192 @@
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { provideNoopAnimations } from "@angular/platform-browser/animations";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { DIALOG_DATA, DialogRef, DialogService } from "@bitwarden/components";
|
||||
|
||||
import {
|
||||
AutofillConfirmationDialogComponent,
|
||||
AutofillConfirmationDialogResult,
|
||||
AutofillConfirmationDialogParams,
|
||||
} from "./autofill-confirmation-dialog.component";
|
||||
|
||||
describe("AutofillConfirmationDialogComponent", () => {
|
||||
let fixture: ComponentFixture<AutofillConfirmationDialogComponent>;
|
||||
let component: AutofillConfirmationDialogComponent;
|
||||
|
||||
const dialogRef = {
|
||||
close: jest.fn(),
|
||||
} as unknown as DialogRef;
|
||||
|
||||
const params: AutofillConfirmationDialogParams = {
|
||||
currentUrl: "https://example.com/path?q=1",
|
||||
savedUrls: ["https://one.example.com/a", "https://two.example.com/b", "not-a-url.example"],
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.spyOn(Utils, "getHostname").mockImplementation((value: string | null | undefined) => {
|
||||
if (typeof value !== "string" || !value) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
// handle non-URL host strings gracefully
|
||||
if (!value.includes("://")) {
|
||||
return value;
|
||||
}
|
||||
return new URL(value).hostname;
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AutofillConfirmationDialogComponent],
|
||||
providers: [
|
||||
provideNoopAnimations(),
|
||||
{ provide: DIALOG_DATA, useValue: params },
|
||||
{ provide: DialogRef, useValue: dialogRef },
|
||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||
{ provide: DialogService, useValue: {} },
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AutofillConfirmationDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("normalizes currentUrl and savedUrls via Utils.getHostname", () => {
|
||||
expect(Utils.getHostname).toHaveBeenCalledTimes(1 + (params.savedUrls?.length ?? 0));
|
||||
// current
|
||||
expect(component.currentUrl).toBe("example.com");
|
||||
// saved
|
||||
expect(component.savedUrls).toEqual([
|
||||
"one.example.com",
|
||||
"two.example.com",
|
||||
"not-a-url.example",
|
||||
]);
|
||||
});
|
||||
|
||||
it("renders normalized values into the template (shallow check)", () => {
|
||||
const text = fixture.nativeElement.textContent as string;
|
||||
expect(text).toContain("example.com");
|
||||
expect(text).toContain("one.example.com");
|
||||
expect(text).toContain("two.example.com");
|
||||
expect(text).toContain("not-a-url.example");
|
||||
});
|
||||
|
||||
it("emits Canceled on close()", () => {
|
||||
const spy = jest.spyOn(dialogRef, "close");
|
||||
component["close"]();
|
||||
expect(spy).toHaveBeenCalledWith(AutofillConfirmationDialogResult.Canceled);
|
||||
});
|
||||
|
||||
it("emits AutofillAndUrlAdded on autofillAndAddUrl()", () => {
|
||||
const spy = jest.spyOn(dialogRef, "close");
|
||||
component["autofillAndAddUrl"]();
|
||||
expect(spy).toHaveBeenCalledWith(AutofillConfirmationDialogResult.AutofillAndUrlAdded);
|
||||
});
|
||||
|
||||
it("emits AutofilledOnly on autofillOnly()", () => {
|
||||
const spy = jest.spyOn(dialogRef, "close");
|
||||
component["autofillOnly"]();
|
||||
expect(spy).toHaveBeenCalledWith(AutofillConfirmationDialogResult.AutofilledOnly);
|
||||
});
|
||||
|
||||
it("applies collapsed list gradient class by default, then clears it after viewAllSavedUrls()", () => {
|
||||
const initial = component["savedUrlsListClass"];
|
||||
expect(initial).toContain("gradient");
|
||||
|
||||
component["viewAllSavedUrls"]();
|
||||
fixture.detectChanges();
|
||||
|
||||
const expanded = component["savedUrlsListClass"];
|
||||
expect(expanded).toBe("");
|
||||
});
|
||||
|
||||
it("handles empty savedUrls gracefully", async () => {
|
||||
const newParams: AutofillConfirmationDialogParams = {
|
||||
currentUrl: "https://bitwarden.com/help",
|
||||
savedUrls: [],
|
||||
};
|
||||
|
||||
const newFixture = TestBed.createComponent(AutofillConfirmationDialogComponent);
|
||||
const newInstance = newFixture.componentInstance;
|
||||
|
||||
(newInstance as any).params = newParams;
|
||||
const fresh = new AutofillConfirmationDialogComponent(
|
||||
newParams as any,
|
||||
dialogRef,
|
||||
) as AutofillConfirmationDialogComponent;
|
||||
|
||||
expect(fresh.savedUrls).toEqual([]);
|
||||
expect(fresh.currentUrl).toBe("bitwarden.com");
|
||||
});
|
||||
|
||||
it("handles undefined savedUrls by defaulting to [] and empty strings from Utils.getHostname", () => {
|
||||
const localParams: AutofillConfirmationDialogParams = {
|
||||
currentUrl: "https://sub.domain.tld/x",
|
||||
};
|
||||
|
||||
const local = new AutofillConfirmationDialogComponent(localParams as any, dialogRef);
|
||||
|
||||
expect(local.savedUrls).toEqual([]);
|
||||
expect(local.currentUrl).toBe("sub.domain.tld");
|
||||
});
|
||||
|
||||
it("filters out falsy/invalid values from Utils.getHostname in savedUrls", () => {
|
||||
(Utils.getHostname as jest.Mock).mockImplementationOnce(() => "example.com");
|
||||
(Utils.getHostname as jest.Mock)
|
||||
.mockImplementationOnce(() => "ok.example")
|
||||
.mockImplementationOnce(() => "")
|
||||
.mockImplementationOnce(() => undefined as unknown as string);
|
||||
|
||||
const edgeParams: AutofillConfirmationDialogParams = {
|
||||
currentUrl: "https://example.com",
|
||||
savedUrls: ["https://ok.example", "://bad", "%%%"],
|
||||
};
|
||||
|
||||
const edge = new AutofillConfirmationDialogComponent(edgeParams as any, dialogRef);
|
||||
|
||||
expect(edge.currentUrl).toBe("example.com");
|
||||
expect(edge.savedUrls).toEqual(["ok.example"]);
|
||||
});
|
||||
|
||||
it("renders one current-url callout and N saved-url callouts", () => {
|
||||
const callouts = Array.from(
|
||||
fixture.nativeElement.querySelectorAll("bit-callout"),
|
||||
) as HTMLElement[];
|
||||
expect(callouts.length).toBe(1 + params.savedUrls!.length);
|
||||
});
|
||||
|
||||
it("renders normalized hostnames into the DOM text", () => {
|
||||
const text = (fixture.nativeElement.textContent as string).replace(/\s+/g, " ");
|
||||
expect(text).toContain("example.com");
|
||||
expect(text).toContain("one.example.com");
|
||||
expect(text).toContain("two.example.com");
|
||||
});
|
||||
|
||||
it("shows the 'view all' button when savedUrls > 1 and hides it after click", () => {
|
||||
const findViewAll = () =>
|
||||
fixture.nativeElement.querySelector(
|
||||
"button.tw-text-sm.tw-font-bold.tw-cursor-pointer",
|
||||
) as HTMLButtonElement | null;
|
||||
|
||||
let btn = findViewAll();
|
||||
expect(btn).toBeTruthy();
|
||||
|
||||
btn!.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
btn = findViewAll();
|
||||
expect(btn).toBeFalsy();
|
||||
expect(component.savedUrlsExpanded).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { ChangeDetectionStrategy, Component, Inject } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values";
|
||||
import {
|
||||
DIALOG_DATA,
|
||||
DialogConfig,
|
||||
DialogRef,
|
||||
ButtonModule,
|
||||
DialogService,
|
||||
DialogModule,
|
||||
TypographyModule,
|
||||
CalloutComponent,
|
||||
LinkModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
export interface AutofillConfirmationDialogParams {
|
||||
savedUrls?: string[];
|
||||
currentUrl: string;
|
||||
}
|
||||
|
||||
export const AutofillConfirmationDialogResult = Object.freeze({
|
||||
AutofillAndUrlAdded: "added",
|
||||
AutofilledOnly: "autofilled",
|
||||
Canceled: "canceled",
|
||||
} as const);
|
||||
|
||||
export type AutofillConfirmationDialogResultType = UnionOfValues<
|
||||
typeof AutofillConfirmationDialogResult
|
||||
>;
|
||||
|
||||
@Component({
|
||||
templateUrl: "./autofill-confirmation-dialog.component.html",
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
ButtonModule,
|
||||
CalloutComponent,
|
||||
CommonModule,
|
||||
DialogModule,
|
||||
LinkModule,
|
||||
TypographyModule,
|
||||
JslibModule,
|
||||
],
|
||||
})
|
||||
export class AutofillConfirmationDialogComponent {
|
||||
AutofillConfirmationDialogResult = AutofillConfirmationDialogResult;
|
||||
|
||||
currentUrl: string = "";
|
||||
savedUrls: string[] = [];
|
||||
savedUrlsExpanded = false;
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected params: AutofillConfirmationDialogParams,
|
||||
private dialogRef: DialogRef,
|
||||
) {
|
||||
this.currentUrl = Utils.getHostname(params.currentUrl);
|
||||
this.savedUrls =
|
||||
params.savedUrls?.map((url) => Utils.getHostname(url) ?? "").filter(Boolean) ?? [];
|
||||
}
|
||||
|
||||
protected get savedUrlsListClass(): string {
|
||||
return this.savedUrlsExpanded
|
||||
? ""
|
||||
: `tw-relative
|
||||
tw-max-h-24
|
||||
tw-overflow-hidden
|
||||
after:tw-pointer-events-none after:tw-content-['']
|
||||
after:tw-absolute after:tw-inset-x-0 after:tw-bottom-0
|
||||
after:tw-h-8 after:tw-bg-gradient-to-t
|
||||
after:tw-from-background after:tw-to-transparent
|
||||
`;
|
||||
}
|
||||
|
||||
protected viewAllSavedUrls() {
|
||||
this.savedUrlsExpanded = true;
|
||||
}
|
||||
|
||||
protected close() {
|
||||
this.dialogRef.close(AutofillConfirmationDialogResult.Canceled);
|
||||
}
|
||||
|
||||
protected autofillAndAddUrl() {
|
||||
this.dialogRef.close(AutofillConfirmationDialogResult.AutofillAndUrlAdded);
|
||||
}
|
||||
|
||||
protected autofillOnly() {
|
||||
this.dialogRef.close(AutofillConfirmationDialogResult.AutofilledOnly);
|
||||
}
|
||||
|
||||
static open(
|
||||
dialogService: DialogService,
|
||||
config: DialogConfig<AutofillConfirmationDialogParams>,
|
||||
) {
|
||||
return dialogService.open<AutofillConfirmationDialogResultType>(
|
||||
AutofillConfirmationDialogComponent,
|
||||
{ ...config },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,8 @@ import { VaultPopupItemsService } from "../../../services/vault-popup-items.serv
|
||||
import { PopupCipherViewLike } from "../../../views/popup-cipher.view";
|
||||
import { VaultListItemsContainerComponent } from "../vault-list-items-container/vault-list-items-container.component";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
imports: [
|
||||
CommonModule,
|
||||
@@ -46,7 +48,7 @@ export class AutofillVaultListItemsComponent {
|
||||
startWith(true), // Start with true to avoid flashing the fill button on first load
|
||||
);
|
||||
|
||||
protected groupByType = toSignal(
|
||||
protected readonly groupByType = toSignal(
|
||||
this.vaultPopupItemsService.hasFilterApplied$.pipe(map((hasFilter) => !hasFilter)),
|
||||
);
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ import { VaultPopupAutofillService } from "../../../services/vault-popup-autofil
|
||||
|
||||
const blockedURISettingsRoute = "/blocked-domains";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
imports: [
|
||||
BannerModule,
|
||||
|
||||
@@ -9,6 +9,8 @@ import { VaultCarouselModule } from "@bitwarden/vault";
|
||||
|
||||
import { IntroCarouselService } from "../../../services/intro-carousel.service";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-intro-carousel",
|
||||
templateUrl: "./intro-carousel.component.html",
|
||||
|
||||
@@ -21,6 +21,8 @@ type CipherItem = {
|
||||
field: CopyAction;
|
||||
};
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-item-copy-actions",
|
||||
templateUrl: "item-copy-actions.component.html",
|
||||
@@ -35,6 +37,8 @@ type CipherItem = {
|
||||
})
|
||||
export class ItemCopyActionsComponent {
|
||||
protected showQuickCopyActions$ = inject(VaultPopupCopyButtonsService).showQuickCopyActions$;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input({ required: true }) cipher!: CipherViewLike;
|
||||
|
||||
protected CipherViewLikeUtils = CipherViewLikeUtils;
|
||||
|
||||
@@ -13,9 +13,17 @@
|
||||
<button type="button" bitMenuItem (click)="doAutofill()">
|
||||
{{ "autofill" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem *ngIf="canEdit && isLogin" (click)="doAutofillAndSave()">
|
||||
{{ "fillAndSave" | i18n }}
|
||||
</button>
|
||||
<!-- Autofill confirmation handles both 'autofill' and 'autofill and save' so no need to show both -->
|
||||
@if (!(showAutofillConfirmation$ | async)) {
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
*ngIf="canEdit && isLogin"
|
||||
(click)="doAutofillAndSave()"
|
||||
>
|
||||
{{ "fillAndSave" | i18n }}
|
||||
</button>
|
||||
}
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="showViewOption">
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
|
||||
import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
|
||||
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { Router } from "@angular/router";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import {
|
||||
UriMatchStrategy,
|
||||
UriMatchStrategySetting,
|
||||
} from "@bitwarden/common/models/domain/domain-service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
|
||||
import { VaultPopupItemsService } from "../../../services/vault-popup-items.service";
|
||||
import {
|
||||
AutofillConfirmationDialogComponent,
|
||||
AutofillConfirmationDialogResult,
|
||||
} from "../autofill-confirmation-dialog/autofill-confirmation-dialog.component";
|
||||
|
||||
import { ItemMoreOptionsComponent } from "./item-more-options.component";
|
||||
|
||||
describe("ItemMoreOptionsComponent", () => {
|
||||
let fixture: ComponentFixture<ItemMoreOptionsComponent>;
|
||||
let component: ItemMoreOptionsComponent;
|
||||
|
||||
const dialogService = {
|
||||
openSimpleDialog: jest.fn().mockResolvedValue(true),
|
||||
open: jest.fn(),
|
||||
};
|
||||
const featureFlag$ = new BehaviorSubject<boolean>(false);
|
||||
const configService = {
|
||||
getFeatureFlag$: jest.fn().mockImplementation(() => featureFlag$.asObservable()),
|
||||
};
|
||||
const cipherService = {
|
||||
getFullCipherView: jest.fn(),
|
||||
encrypt: jest.fn(),
|
||||
updateWithServer: jest.fn(),
|
||||
softDeleteWithServer: jest.fn(),
|
||||
};
|
||||
const autofillSvc = {
|
||||
doAutofill: jest.fn(),
|
||||
doAutofillAndSave: jest.fn(),
|
||||
currentAutofillTab$: new BehaviorSubject<{ url?: string | null } | null>(null),
|
||||
autofillAllowed$: new BehaviorSubject(true),
|
||||
};
|
||||
|
||||
const uriMatchStrategy$ = new BehaviorSubject<UriMatchStrategySetting>(UriMatchStrategy.Domain);
|
||||
|
||||
const domainSettingsService = {
|
||||
resolvedDefaultUriMatchStrategy$: uriMatchStrategy$.asObservable(),
|
||||
};
|
||||
|
||||
const hasSearchText$ = new BehaviorSubject(false);
|
||||
const vaultPopupItemsService = {
|
||||
hasSearchText$: hasSearchText$.asObservable(),
|
||||
};
|
||||
|
||||
const baseCipher = {
|
||||
id: "cipher-1",
|
||||
login: {
|
||||
uris: [
|
||||
{ uri: "https://one.example.com" },
|
||||
{ uri: "" },
|
||||
{ uri: undefined as unknown as string },
|
||||
{ uri: "https://two.example.com/a" },
|
||||
],
|
||||
username: "user",
|
||||
},
|
||||
favorite: false,
|
||||
reprompt: 0,
|
||||
type: CipherType.Login,
|
||||
viewPassword: true,
|
||||
edit: true,
|
||||
} as any;
|
||||
|
||||
beforeEach(waitForAsync(async () => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
cipherService.getFullCipherView.mockImplementation(async (c) => ({ ...baseCipher, ...c }));
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ItemMoreOptionsComponent, NoopAnimationsModule],
|
||||
providers: [
|
||||
{ provide: ConfigService, useValue: configService },
|
||||
{ provide: CipherService, useValue: cipherService },
|
||||
{ provide: VaultPopupAutofillService, useValue: autofillSvc },
|
||||
|
||||
{ provide: I18nService, useValue: { t: (k: string) => k } },
|
||||
{ provide: AccountService, useValue: { activeAccount$: of({ id: "UserId" }) } },
|
||||
{ provide: OrganizationService, useValue: { hasOrganizations: () => of(false) } },
|
||||
{
|
||||
provide: CipherAuthorizationService,
|
||||
useValue: { canDeleteCipher$: () => of(true), canCloneCipher$: () => of(true) },
|
||||
},
|
||||
{ provide: CollectionService, useValue: { decryptedCollections$: () => of([]) } },
|
||||
{ provide: RestrictedItemTypesService, useValue: { restricted$: of([]) } },
|
||||
{ provide: CipherArchiveService, useValue: { userCanArchive$: () => of(true) } },
|
||||
{ provide: ToastService, useValue: { showToast: () => {} } },
|
||||
{ provide: Router, useValue: { navigate: () => Promise.resolve(true) } },
|
||||
{ provide: PasswordRepromptService, useValue: mock<PasswordRepromptService>() },
|
||||
{
|
||||
provide: DomainSettingsService,
|
||||
useValue: domainSettingsService,
|
||||
},
|
||||
{
|
||||
provide: VaultPopupItemsService,
|
||||
useValue: vaultPopupItemsService,
|
||||
},
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
});
|
||||
TestBed.overrideProvider(DialogService, { useValue: dialogService });
|
||||
await TestBed.compileComponents();
|
||||
fixture = TestBed.createComponent(ItemMoreOptionsComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.cipher = baseCipher;
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
function mockConfirmDialogResult(result: string) {
|
||||
const openSpy = jest
|
||||
.spyOn(AutofillConfirmationDialogComponent, "open")
|
||||
.mockReturnValue({ closed: of(result) } as any);
|
||||
return openSpy;
|
||||
}
|
||||
|
||||
it("calls doAutofill without showing the confirmation dialog when the feature flag is disabled or search text is not present", async () => {
|
||||
autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com" });
|
||||
|
||||
await component.doAutofill();
|
||||
|
||||
expect(cipherService.getFullCipherView).toHaveBeenCalled();
|
||||
expect(autofillSvc.doAutofill).toHaveBeenCalledTimes(1);
|
||||
expect(autofillSvc.doAutofill).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ id: "cipher-1" }),
|
||||
false,
|
||||
);
|
||||
expect(autofillSvc.doAutofillAndSave).not.toHaveBeenCalled();
|
||||
expect(dialogService.openSimpleDialog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("opens the confirmation dialog with filtered saved URLs when the feature flag is enabled and search text is present", async () => {
|
||||
featureFlag$.next(true);
|
||||
hasSearchText$.next(true);
|
||||
autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com/path" });
|
||||
const openSpy = mockConfirmDialogResult(AutofillConfirmationDialogResult.Canceled);
|
||||
|
||||
await component.doAutofill();
|
||||
|
||||
expect(openSpy).toHaveBeenCalledTimes(1);
|
||||
const args = openSpy.mock.calls[0][1];
|
||||
expect(args.data.currentUrl).toBe("https://page.example.com/path");
|
||||
expect(args.data.savedUrls).toEqual(["https://one.example.com", "https://two.example.com/a"]);
|
||||
});
|
||||
|
||||
it("does nothing when the user cancels the autofill confirmation dialog", async () => {
|
||||
featureFlag$.next(true);
|
||||
autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com" });
|
||||
mockConfirmDialogResult(AutofillConfirmationDialogResult.Canceled);
|
||||
|
||||
await component.doAutofill();
|
||||
|
||||
expect(autofillSvc.doAutofill).not.toHaveBeenCalled();
|
||||
expect(autofillSvc.doAutofillAndSave).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("autofills the item without adding the URL when the user selects 'AutofilledOnly'", async () => {
|
||||
featureFlag$.next(true);
|
||||
autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com" });
|
||||
mockConfirmDialogResult(AutofillConfirmationDialogResult.AutofilledOnly);
|
||||
|
||||
await component.doAutofill();
|
||||
|
||||
expect(autofillSvc.doAutofill).toHaveBeenCalledTimes(1);
|
||||
expect(autofillSvc.doAutofillAndSave).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("autofills the item and adds the URL when the user selects 'AutofillAndUrlAdded'", async () => {
|
||||
featureFlag$.next(true);
|
||||
autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com" });
|
||||
mockConfirmDialogResult(AutofillConfirmationDialogResult.AutofillAndUrlAdded);
|
||||
|
||||
await component.doAutofill();
|
||||
|
||||
expect(autofillSvc.doAutofillAndSave).toHaveBeenCalledTimes(1);
|
||||
expect(autofillSvc.doAutofillAndSave.mock.calls[0][1]).toBe(false);
|
||||
expect(autofillSvc.doAutofill).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("only shows the exact match dialog when the uri match strategy is Exact and no URIs match", async () => {
|
||||
featureFlag$.next(true);
|
||||
uriMatchStrategy$.next(UriMatchStrategy.Exact);
|
||||
hasSearchText$.next(true);
|
||||
autofillSvc.currentAutofillTab$.next({ url: "https://no-match.example.com" });
|
||||
|
||||
await component.doAutofill();
|
||||
|
||||
expect(dialogService.openSimpleDialog).toHaveBeenCalledTimes(1);
|
||||
expect(dialogService.openSimpleDialog).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
title: expect.objectContaining({ key: "cannotAutofill" }),
|
||||
content: expect.objectContaining({ key: "cannotAutofillExactMatch" }),
|
||||
type: "info",
|
||||
}),
|
||||
);
|
||||
expect(autofillSvc.doAutofill).not.toHaveBeenCalled();
|
||||
expect(autofillSvc.doAutofillAndSave).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("hides the 'Fill and Save' button when showAutofillConfirmation$ is true", async () => {
|
||||
// Enable both feature flag and search text → makes showAutofillConfirmation$ true
|
||||
featureFlag$.next(true);
|
||||
hasSearchText$.next(true);
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
const fillAndSaveButton = fixture.nativeElement.querySelector(
|
||||
"button[bitMenuItem]:not([disabled])",
|
||||
);
|
||||
|
||||
const buttonText = fillAndSaveButton?.textContent?.trim().toLowerCase() ?? "";
|
||||
expect(buttonText.includes("fillAndSave".toLowerCase())).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { booleanAttribute, Component, Input } from "@angular/core";
|
||||
import { Router, RouterModule } from "@angular/router";
|
||||
@@ -11,8 +9,12 @@ import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherId } from "@bitwarden/common/types/guid";
|
||||
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
|
||||
@@ -32,16 +34,25 @@ import {
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
|
||||
import { VaultPopupItemsService } from "../../../services/vault-popup-items.service";
|
||||
import { AddEditQueryParams } from "../add-edit/add-edit-v2.component";
|
||||
import {
|
||||
AutofillConfirmationDialogComponent,
|
||||
AutofillConfirmationDialogResult,
|
||||
} from "../autofill-confirmation-dialog/autofill-confirmation-dialog.component";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-item-more-options",
|
||||
templateUrl: "./item-more-options.component.html",
|
||||
imports: [ItemModule, IconButtonModule, MenuModule, CommonModule, JslibModule, RouterModule],
|
||||
})
|
||||
export class ItemMoreOptionsComponent {
|
||||
private _cipher$ = new BehaviorSubject<CipherViewLike>(undefined);
|
||||
private _cipher$ = new BehaviorSubject<CipherViewLike>({} as CipherViewLike);
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input({
|
||||
required: true,
|
||||
})
|
||||
@@ -57,18 +68,29 @@ export class ItemMoreOptionsComponent {
|
||||
* Flag to show view item menu option. Used when something else is
|
||||
* assigned as the primary action for the item, such as autofill.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input({ transform: booleanAttribute })
|
||||
showViewOption: boolean;
|
||||
showViewOption = false;
|
||||
|
||||
/**
|
||||
* Flag to hide the autofill menu options. Used for items that are
|
||||
* already in the autofill list suggestion.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input({ transform: booleanAttribute })
|
||||
hideAutofillOptions: boolean;
|
||||
hideAutofillOptions = false;
|
||||
|
||||
protected autofillAllowed$ = this.vaultPopupAutofillService.autofillAllowed$;
|
||||
|
||||
protected showAutofillConfirmation$ = combineLatest([
|
||||
this.configService.getFeatureFlag$(FeatureFlag.AutofillConfirmation),
|
||||
this.vaultPopupItemsService.hasSearchText$,
|
||||
]).pipe(map(([isFeatureFlagEnabled, hasSearchText]) => isFeatureFlagEnabled && hasSearchText));
|
||||
|
||||
protected uriMatchStrategy$ = this.domainSettingsService.resolvedDefaultUriMatchStrategy$;
|
||||
|
||||
/**
|
||||
* Observable that emits a boolean value indicating if the user is authorized to clone the cipher.
|
||||
* @protected
|
||||
@@ -138,6 +160,9 @@ export class ItemMoreOptionsComponent {
|
||||
private collectionService: CollectionService,
|
||||
private restrictedItemTypesService: RestrictedItemTypesService,
|
||||
private cipherArchiveService: CipherArchiveService,
|
||||
private configService: ConfigService,
|
||||
private vaultPopupItemsService: VaultPopupItemsService,
|
||||
private domainSettingsService: DomainSettingsService,
|
||||
) {}
|
||||
|
||||
get canEdit() {
|
||||
@@ -169,14 +194,63 @@ export class ItemMoreOptionsComponent {
|
||||
return this.cipher.favorite ? "unfavorite" : "favorite";
|
||||
}
|
||||
|
||||
async doAutofill() {
|
||||
const cipher = await this.cipherService.getFullCipherView(this.cipher);
|
||||
await this.vaultPopupAutofillService.doAutofill(cipher);
|
||||
}
|
||||
|
||||
async doAutofillAndSave() {
|
||||
const cipher = await this.cipherService.getFullCipherView(this.cipher);
|
||||
await this.vaultPopupAutofillService.doAutofillAndSave(cipher, false);
|
||||
await this.vaultPopupAutofillService.doAutofillAndSave(cipher);
|
||||
}
|
||||
|
||||
async doAutofill() {
|
||||
const cipher = await this.cipherService.getFullCipherView(this.cipher);
|
||||
|
||||
const showAutofillConfirmation = await firstValueFrom(this.showAutofillConfirmation$);
|
||||
|
||||
if (!showAutofillConfirmation) {
|
||||
await this.vaultPopupAutofillService.doAutofill(cipher, false);
|
||||
return;
|
||||
}
|
||||
|
||||
const uriMatchStrategy = await firstValueFrom(this.uriMatchStrategy$);
|
||||
if (uriMatchStrategy === UriMatchStrategy.Exact) {
|
||||
await this.dialogService.openSimpleDialog({
|
||||
title: { key: "cannotAutofill" },
|
||||
content: { key: "cannotAutofillExactMatch" },
|
||||
type: "info",
|
||||
acceptButtonText: { key: "okay" },
|
||||
cancelButtonText: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTab = await firstValueFrom(this.vaultPopupAutofillService.currentAutofillTab$);
|
||||
|
||||
if (!currentTab?.url) {
|
||||
await this.dialogService.openSimpleDialog({
|
||||
title: { key: "error" },
|
||||
content: { key: "errorGettingAutoFillData" },
|
||||
type: "danger",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const ref = AutofillConfirmationDialogComponent.open(this.dialogService, {
|
||||
data: {
|
||||
currentUrl: currentTab?.url || "",
|
||||
savedUrls: cipher.login?.uris?.filter((u) => u.uri).map((u) => u.uri!) ?? [],
|
||||
},
|
||||
});
|
||||
|
||||
const result = await firstValueFrom(ref.closed);
|
||||
|
||||
switch (result) {
|
||||
case AutofillConfirmationDialogResult.Canceled:
|
||||
return;
|
||||
case AutofillConfirmationDialogResult.AutofilledOnly:
|
||||
await this.vaultPopupAutofillService.doAutofill(cipher);
|
||||
return;
|
||||
case AutofillConfirmationDialogResult.AutofillAndUrlAdded:
|
||||
await this.vaultPopupAutofillService.doAutofillAndSave(cipher, false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async onView() {
|
||||
@@ -196,15 +270,14 @@ export class ItemMoreOptionsComponent {
|
||||
const cipher = await this.cipherService.getFullCipherView(this.cipher);
|
||||
|
||||
cipher.favorite = !cipher.favorite;
|
||||
const activeUserId = await firstValueFrom(
|
||||
const activeUserId = (await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
)) as UserId;
|
||||
|
||||
const encryptedCipher = await this.cipherService.encrypt(cipher, activeUserId);
|
||||
await this.cipherService.updateWithServer(encryptedCipher);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t(
|
||||
this.cipher.favorite ? "itemAddedToFavorites" : "itemRemovedFromFavorites",
|
||||
),
|
||||
|
||||
@@ -23,6 +23,8 @@ export interface NewItemInitialValues {
|
||||
collectionId?: CollectionId;
|
||||
}
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-new-item-dropdown",
|
||||
templateUrl: "new-item-dropdown-v2.component.html",
|
||||
@@ -34,6 +36,8 @@ export class NewItemDropdownV2Component implements OnInit {
|
||||
/**
|
||||
* Optional initial values to pass to the add cipher form
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input()
|
||||
initialValues: NewItemInitialValues;
|
||||
|
||||
|
||||
@@ -18,14 +18,24 @@ import {
|
||||
VaultGeneratorDialogComponent,
|
||||
} from "./vault-generator-dialog.component";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "vault-cipher-form-generator",
|
||||
template: "",
|
||||
})
|
||||
class MockCipherFormGenerator {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() type: "password" | "username" = "password";
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
|
||||
@Output() algorithmSelected: EventEmitter<AlgorithmInfo> = new EventEmitter();
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() uri: string = "";
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
|
||||
@Output() valueGenerated = new EventEmitter<string>();
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@ export const GeneratorDialogAction = {
|
||||
|
||||
type GeneratorDialogAction = UnionOfValues<typeof GeneratorDialogAction>;
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-vault-generator-dialog",
|
||||
templateUrl: "./vault-generator-dialog.component.html",
|
||||
|
||||
@@ -17,6 +17,8 @@ import { VaultPopupListFiltersService } from "../../../../../vault/popup/service
|
||||
import { VaultListFiltersComponent } from "../vault-list-filters/vault-list-filters.component";
|
||||
import { VaultV2SearchComponent } from "../vault-search/vault-v2-search.component";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-vault-header-v2",
|
||||
templateUrl: "vault-header-v2.component.html",
|
||||
@@ -31,6 +33,8 @@ import { VaultV2SearchComponent } from "../vault-search/vault-v2-search.componen
|
||||
],
|
||||
})
|
||||
export class VaultHeaderV2Component {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@ViewChild(DisclosureComponent) disclosure: DisclosureComponent;
|
||||
|
||||
/** Emits the visibility status of the disclosure component. */
|
||||
|
||||
@@ -8,6 +8,8 @@ import { ChipSelectComponent } from "@bitwarden/components";
|
||||
|
||||
import { VaultPopupListFiltersService } from "../../../services/vault-popup-list-filters.service";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-vault-list-filters",
|
||||
templateUrl: "./vault-list-filters.component.html",
|
||||
|
||||
@@ -90,12 +90,18 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
private vaultPopupSectionService = inject(VaultPopupSectionService);
|
||||
protected CipherViewLikeUtils = CipherViewLikeUtils;
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@ViewChild(CdkVirtualScrollViewport, { static: false }) viewPort!: CdkVirtualScrollViewport;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@ViewChild(DisclosureComponent) disclosure!: DisclosureComponent;
|
||||
|
||||
/**
|
||||
* Indicates whether the section should be open or closed if collapsibleKey is provided
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
protected sectionOpenState: Signal<boolean> = computed(() => {
|
||||
if (!this.collapsibleKey()) {
|
||||
return true;
|
||||
@@ -130,17 +136,23 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
*/
|
||||
private viewCipherTimeout?: number;
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
ciphers = input<PopupCipherViewLike[]>([]);
|
||||
|
||||
/**
|
||||
* If true, we will group ciphers by type (Login, Card, Identity)
|
||||
* within subheadings in a single container, converted to a WritableSignal.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
groupByType = input<boolean | undefined>(false);
|
||||
|
||||
/**
|
||||
* Computed signal for a grouped list of ciphers with an optional header
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
cipherGroups = computed<
|
||||
{
|
||||
subHeaderKey?: string;
|
||||
@@ -183,6 +195,8 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
/**
|
||||
* Title for the vault list item section.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
title = input<string | undefined>(undefined);
|
||||
|
||||
/**
|
||||
@@ -191,33 +205,45 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
* The key must be added to the state definition in `vault-popup-section.service.ts` since the
|
||||
* collapsed state is stored locally.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
collapsibleKey = input<keyof PopupSectionOpen | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Optional description for the vault list item section. Will be shown below the title even when
|
||||
* no ciphers are available.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
description = input<string | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Option to show a refresh button in the section header.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
showRefresh = input(false, { transform: booleanAttribute });
|
||||
|
||||
/**
|
||||
* Event emitted when the refresh button is clicked.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
|
||||
@Output()
|
||||
onRefresh = new EventEmitter<void>();
|
||||
|
||||
/**
|
||||
* Flag indicating that the current tab location is blocked
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
currentURIIsBlocked = toSignal(this.vaultPopupAutofillService.currentTabIsOnBlocklist$);
|
||||
|
||||
/**
|
||||
* Resolved i18n key to use for suggested cipher items
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
cipherItemTitleKey = computed(() => {
|
||||
return (cipher: CipherViewLike) => {
|
||||
const login = CipherViewLikeUtils.getLogin(cipher);
|
||||
@@ -233,11 +259,15 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
/**
|
||||
* Option to show the autofill button for each item.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
showAutofillButton = input(false, { transform: booleanAttribute });
|
||||
|
||||
/**
|
||||
* Flag indicating whether the suggested cipher item autofill button should be shown or not
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
hideAutofillButton = computed(
|
||||
() => !this.showAutofillButton() || this.currentURIIsBlocked() || this.primaryActionAutofill(),
|
||||
);
|
||||
@@ -245,22 +275,30 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
/**
|
||||
* Flag indicating whether the cipher item autofill menu options should be shown or not
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
hideAutofillMenuOptions = computed(() => this.currentURIIsBlocked() || this.showAutofillButton());
|
||||
|
||||
/**
|
||||
* Option to perform autofill operation as the primary action for autofill suggestions.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
primaryActionAutofill = input(false, { transform: booleanAttribute });
|
||||
|
||||
/**
|
||||
* Remove the bottom margin from the bit-section in this component
|
||||
* (used for containers at the end of the page where bottom margin is not needed)
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
disableSectionMargin = input(false, { transform: booleanAttribute });
|
||||
|
||||
/**
|
||||
* Remove the description margin
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
disableDescriptionMargin = input(false, { transform: booleanAttribute });
|
||||
|
||||
/**
|
||||
@@ -275,6 +313,8 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
return collections[0]?.name;
|
||||
}
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
protected autofillShortcutTooltip = signal<string | undefined>(undefined);
|
||||
|
||||
constructor(
|
||||
|
||||
@@ -18,6 +18,8 @@ import { PopupHeaderComponent } from "../../../../../platform/popup/layout/popup
|
||||
import { PopupPageComponent } from "../../../../../platform/popup/layout/popup-page.component";
|
||||
import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "vault-password-history-v2",
|
||||
templateUrl: "vault-password-history-v2.component.html",
|
||||
|
||||
@@ -10,6 +10,8 @@ import { SearchModule } from "@bitwarden/components";
|
||||
|
||||
import { VaultPopupItemsService } from "../../../services/vault-popup-items.service";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
imports: [CommonModule, SearchModule, JslibModule, FormsModule],
|
||||
selector: "app-vault-v2-search",
|
||||
|
||||
@@ -64,6 +64,8 @@ const VaultState = {
|
||||
|
||||
type VaultState = UnionOfValues<typeof VaultState>;
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-vault",
|
||||
templateUrl: "vault-v2.component.html",
|
||||
@@ -89,6 +91,8 @@ type VaultState = UnionOfValues<typeof VaultState>;
|
||||
],
|
||||
})
|
||||
export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@ViewChild(CdkVirtualScrollableElement) virtualScrollElement?: CdkVirtualScrollableElement;
|
||||
|
||||
NudgeType = NudgeType;
|
||||
|
||||
@@ -76,6 +76,8 @@ type LoadAction =
|
||||
| typeof COPY_VERIFICATION_CODE_ID
|
||||
| typeof UPDATE_PASSWORD;
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-view-v2",
|
||||
templateUrl: "view-v2.component.html",
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { RouterStateSnapshot } from "@angular/router";
|
||||
|
||||
import { VaultV2Component } from "../components/vault-v2/vault-v2.component";
|
||||
import { VaultPopupItemsService } from "../services/vault-popup-items.service";
|
||||
import { VaultPopupListFiltersService } from "../services/vault-popup-list-filters.service";
|
||||
|
||||
import { clearVaultStateGuard } from "./clear-vault-state.guard";
|
||||
|
||||
describe("clearVaultStateGuard", () => {
|
||||
let applyFilterSpy: jest.Mock;
|
||||
let resetFilterFormSpy: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
applyFilterSpy = jest.fn();
|
||||
resetFilterFormSpy = jest.fn();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: VaultPopupItemsService,
|
||||
useValue: { applyFilter: applyFilterSpy },
|
||||
},
|
||||
{
|
||||
provide: VaultPopupListFiltersService,
|
||||
useValue: { resetFilterForm: resetFilterFormSpy },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it.each([
|
||||
"/view-cipher?cipherId=123",
|
||||
"/edit-cipher?cipherId=123",
|
||||
"/clone-cipher?cipherId=123",
|
||||
"/assign-collections?cipherId=123",
|
||||
])("should not clear vault state when viewing or editing a cipher: %s", (url) => {
|
||||
const nextState = { url } as RouterStateSnapshot;
|
||||
|
||||
const result = TestBed.runInInjectionContext(() =>
|
||||
clearVaultStateGuard({} as VaultV2Component, null, null, nextState),
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(applyFilterSpy).not.toHaveBeenCalled();
|
||||
expect(resetFilterFormSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each(["/settings", "/tabs/settings"])(
|
||||
"should clear vault state when navigating to non-cipher routes: %s",
|
||||
(url) => {
|
||||
const nextState = { url } as RouterStateSnapshot;
|
||||
|
||||
const result = TestBed.runInInjectionContext(() =>
|
||||
clearVaultStateGuard({} as VaultV2Component, null, null, nextState),
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(applyFilterSpy).toHaveBeenCalledWith("");
|
||||
expect(resetFilterFormSpy).toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it("should not clear vault state when not changing states", () => {
|
||||
const result = TestBed.runInInjectionContext(() =>
|
||||
clearVaultStateGuard({} as VaultV2Component, null, null, null),
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(applyFilterSpy).not.toHaveBeenCalled();
|
||||
expect(resetFilterFormSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,8 @@ import { VaultPopupListFiltersService } from "../services/vault-popup-list-filte
|
||||
|
||||
/**
|
||||
* Guard to clear the vault state (search and filter) when navigating away from the vault view.
|
||||
* This ensures the search and filter state is reset when navigating between different tabs, except viewing a cipher.
|
||||
* This ensures the search and filter state is reset when navigating between different tabs,
|
||||
* except viewing or editing a cipher.
|
||||
*/
|
||||
export const clearVaultStateGuard: CanDeactivateFn<VaultV2Component> = (
|
||||
component: VaultV2Component,
|
||||
@@ -17,7 +18,7 @@ export const clearVaultStateGuard: CanDeactivateFn<VaultV2Component> = (
|
||||
) => {
|
||||
const vaultPopupItemsService = inject(VaultPopupItemsService);
|
||||
const vaultPopupListFiltersService = inject(VaultPopupListFiltersService);
|
||||
if (nextState && !isViewingCipher(nextState.url)) {
|
||||
if (nextState && !isCipherOpen(nextState.url)) {
|
||||
vaultPopupItemsService.applyFilter("");
|
||||
vaultPopupListFiltersService.resetFilterForm();
|
||||
}
|
||||
@@ -25,4 +26,8 @@ export const clearVaultStateGuard: CanDeactivateFn<VaultV2Component> = (
|
||||
return true;
|
||||
};
|
||||
|
||||
const isViewingCipher = (url: string): boolean => url.includes("view-cipher");
|
||||
const isCipherOpen = (url: string): boolean =>
|
||||
url.includes("view-cipher") ||
|
||||
url.includes("assign-collections") ||
|
||||
url.includes("edit-cipher") ||
|
||||
url.includes("clone-cipher");
|
||||
|
||||
@@ -261,6 +261,13 @@ export class VaultPopupItemsService {
|
||||
this.remainingCiphers$.pipe(map(() => false)),
|
||||
).pipe(startWith(true), distinctUntilChanged(), shareReplay({ refCount: false, bufferSize: 1 }));
|
||||
|
||||
/** Observable that indicates whether there is search text present.
|
||||
*/
|
||||
hasSearchText$: Observable<boolean> = this._hasSearchText.pipe(
|
||||
distinctUntilChanged(),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
/**
|
||||
* Observable that indicates whether a filter or search text is currently applied to the ciphers.
|
||||
*/
|
||||
|
||||
@@ -31,7 +31,7 @@ export class VaultPopupSectionService {
|
||||
private vaultPopupItemsService = inject(VaultPopupItemsService);
|
||||
private stateProvider = inject(StateProvider);
|
||||
|
||||
private hasFilterOrSearchApplied = toSignal(
|
||||
private readonly hasFilterOrSearchApplied = toSignal(
|
||||
this.vaultPopupItemsService.hasFilterApplied$.pipe(map((hasFilter) => hasFilter)),
|
||||
);
|
||||
|
||||
@@ -40,7 +40,7 @@ export class VaultPopupSectionService {
|
||||
* application-applied overrides.
|
||||
* `null` means there is no current override
|
||||
*/
|
||||
private temporaryStateOverride = signal<Partial<PopupSectionOpen> | null>(null);
|
||||
private readonly temporaryStateOverride = signal<Partial<PopupSectionOpen> | null>(null);
|
||||
|
||||
constructor() {
|
||||
effect(
|
||||
@@ -71,7 +71,7 @@ export class VaultPopupSectionService {
|
||||
* Stored disk state for the open/close state of the sections, with an initial value provided
|
||||
* if the stored disk state does not yet exist.
|
||||
*/
|
||||
private sectionOpenStoredState = toSignal<PopupSectionOpen | null>(
|
||||
private readonly sectionOpenStoredState = toSignal<PopupSectionOpen | null>(
|
||||
this.sectionOpenStateProvider.state$.pipe(map((sectionOpen) => sectionOpen ?? INITIAL_OPEN)),
|
||||
// Indicates that the state value is loading
|
||||
{ initialValue: null },
|
||||
@@ -81,7 +81,7 @@ export class VaultPopupSectionService {
|
||||
* Indicates the current open/close display state of each section, accounting for temporary
|
||||
* non-persisted overrides.
|
||||
*/
|
||||
sectionOpenDisplayState: Signal<Partial<PopupSectionOpen>> = computed(() => ({
|
||||
readonly sectionOpenDisplayState: Signal<Partial<PopupSectionOpen>> = computed(() => ({
|
||||
...this.sectionOpenStoredState(),
|
||||
...this.temporaryStateOverride(),
|
||||
}));
|
||||
|
||||
@@ -22,20 +22,30 @@ import { VaultPopupCopyButtonsService } from "../services/vault-popup-copy-butto
|
||||
|
||||
import { AppearanceV2Component } from "./appearance-v2.component";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "popup-header",
|
||||
template: `<ng-content></ng-content>`,
|
||||
})
|
||||
class MockPopupHeaderComponent {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() pageTitle: string;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() backAction: () => void;
|
||||
}
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "popup-page",
|
||||
template: `<ng-content></ng-content>`,
|
||||
})
|
||||
class MockPopupPageComponent {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() loading: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
|
||||
import { PopupSizeService } from "../../../platform/popup/layout/popup-size.service";
|
||||
import { VaultPopupCopyButtonsService } from "../services/vault-popup-copy-buttons.service";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
templateUrl: "./appearance-v2.component.html",
|
||||
imports: [
|
||||
|
||||
@@ -33,6 +33,8 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp
|
||||
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
templateUrl: "archive.component.html",
|
||||
standalone: true,
|
||||
|
||||
@@ -13,6 +13,8 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp
|
||||
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
templateUrl: "download-bitwarden.component.html",
|
||||
imports: [
|
||||
|
||||
@@ -21,20 +21,30 @@ import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-heade
|
||||
|
||||
import { FoldersV2Component } from "./folders-v2.component";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "popup-header",
|
||||
template: `<ng-content></ng-content>`,
|
||||
})
|
||||
class MockPopupHeaderComponent {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() pageTitle: string = "";
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() backAction: () => void = () => {};
|
||||
}
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "popup-footer",
|
||||
template: `<ng-content></ng-content>`,
|
||||
})
|
||||
class MockPopupFooterComponent {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() pageTitle: string = "";
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp
|
||||
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
templateUrl: "./folders-v2.component.html",
|
||||
imports: [
|
||||
|
||||
@@ -17,6 +17,8 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp
|
||||
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
templateUrl: "more-from-bitwarden-page-v2.component.html",
|
||||
imports: [
|
||||
|
||||
@@ -53,9 +53,13 @@ export class TrashListItemsContainerComponent {
|
||||
/**
|
||||
* The list of trashed items to display.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input()
|
||||
ciphers: PopupCipherViewLike[] = [];
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input()
|
||||
headerText: string;
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp
|
||||
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
templateUrl: "vault-settings-v2.component.html",
|
||||
imports: [
|
||||
@@ -37,12 +39,12 @@ export class VaultSettingsV2Component implements OnInit, OnDestroy {
|
||||
private userId$ = this.accountService.activeAccount$.pipe(getUserId);
|
||||
|
||||
// Check if user is premium user, they will be able to archive items
|
||||
protected userCanArchive = toSignal(
|
||||
protected readonly userCanArchive = toSignal(
|
||||
this.userId$.pipe(switchMap((userId) => this.cipherArchiveService.userCanArchive$(userId))),
|
||||
);
|
||||
|
||||
// Check if user has archived items (does not check if user is premium)
|
||||
protected showArchiveFilter = toSignal(
|
||||
protected readonly showArchiveFilter = toSignal(
|
||||
this.userId$.pipe(switchMap((userId) => this.cipherArchiveService.showArchiveVault$(userId))),
|
||||
);
|
||||
|
||||
|
||||
@@ -36,7 +36,8 @@ const DEFAULT_PARAMS = {
|
||||
* outputPath?: string;
|
||||
* mode?: string;
|
||||
* env?: string;
|
||||
* additionalEntries?: { [outputPath: string]: string }
|
||||
* additionalEntries?: { [outputPath: string]: string };
|
||||
* importAliases?: import("webpack").ResolveOptions["alias"];
|
||||
* }} params - The input parameters for building the config.
|
||||
*/
|
||||
module.exports.buildConfig = function buildConfig(params) {
|
||||
@@ -362,6 +363,7 @@ module.exports.buildConfig = function buildConfig(params) {
|
||||
path: require.resolve("path-browserify"),
|
||||
},
|
||||
cache: true,
|
||||
alias: params.importAliases,
|
||||
},
|
||||
output: {
|
||||
filename: "[name].js",
|
||||
@@ -482,6 +484,7 @@ module.exports.buildConfig = function buildConfig(params) {
|
||||
path: require.resolve("path-browserify"),
|
||||
},
|
||||
cache: true,
|
||||
alias: params.importAliases,
|
||||
},
|
||||
dependencies: ["main"],
|
||||
plugins: [...requiredPlugins, new AngularCheckPlugin()],
|
||||
|
||||
@@ -31,6 +31,7 @@ const DEFAULT_PARAMS = {
|
||||
* localesPath?: string;
|
||||
* externalsModulesDir?: string;
|
||||
* watch?: boolean;
|
||||
* importAliases?: import("webpack").ResolveOptions["alias"];
|
||||
* }} params
|
||||
*/
|
||||
module.exports.buildConfig = function buildConfig(params) {
|
||||
@@ -95,6 +96,7 @@ module.exports.buildConfig = function buildConfig(params) {
|
||||
symlinks: false,
|
||||
modules: params.modulesPath,
|
||||
plugins: [new TsconfigPathsPlugin({ configFile: params.tsConfig })],
|
||||
alias: params.importAliases,
|
||||
},
|
||||
output: {
|
||||
filename: "[name].js",
|
||||
|
||||
104
apps/desktop/desktop_native/Cargo.lock
generated
104
apps/desktop/desktop_native/Cargo.lock
generated
@@ -440,33 +440,6 @@ dependencies = [
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitwarden_chromium_importer"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"base64",
|
||||
"cbc",
|
||||
"hex",
|
||||
"homedir",
|
||||
"napi",
|
||||
"napi-derive",
|
||||
"oo7",
|
||||
"pbkdf2",
|
||||
"rand 0.9.1",
|
||||
"rusqlite",
|
||||
"security-framework",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha1",
|
||||
"tokio",
|
||||
"winapi",
|
||||
"windows 0.61.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
@@ -606,6 +579,31 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chromium_importer"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"base64",
|
||||
"cbc",
|
||||
"hex",
|
||||
"homedir",
|
||||
"oo7",
|
||||
"pbkdf2",
|
||||
"rand 0.9.1",
|
||||
"rusqlite",
|
||||
"security-framework",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha1",
|
||||
"tokio",
|
||||
"winapi",
|
||||
"windows 0.61.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
@@ -968,7 +966,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"autotype",
|
||||
"base64",
|
||||
"bitwarden_chromium_importer",
|
||||
"chromium_importer",
|
||||
"desktop_core",
|
||||
"hex",
|
||||
"napi",
|
||||
@@ -3982,7 +3980,7 @@ dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core 0.61.0",
|
||||
"windows-future",
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
"windows-numerics",
|
||||
]
|
||||
|
||||
@@ -4015,9 +4013,9 @@ checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
|
||||
dependencies = [
|
||||
"windows-implement 0.60.0",
|
||||
"windows-interface 0.59.1",
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
"windows-result 0.3.4",
|
||||
"windows-strings",
|
||||
"windows-strings 0.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4027,7 +4025,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32"
|
||||
dependencies = [
|
||||
"windows-core 0.61.0",
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4080,6 +4078,12 @@ version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.2.0"
|
||||
@@ -4087,18 +4091,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
|
||||
dependencies = [
|
||||
"windows-core 0.61.0",
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.5.3"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
|
||||
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-result 0.3.4",
|
||||
"windows-strings",
|
||||
"windows-link 0.2.1",
|
||||
"windows-result 0.4.1",
|
||||
"windows-strings 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4116,7 +4120,16 @@ version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4125,7 +4138,16 @@ version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4216,7 +4238,7 @@ version = "0.53.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.1.3",
|
||||
"windows_aarch64_gnullvm 0.53.0",
|
||||
"windows_aarch64_msvc 0.53.0",
|
||||
"windows_i686_gnu 0.53.0",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
resolver = "2"
|
||||
members = [
|
||||
"autotype",
|
||||
"bitwarden_chromium_importer",
|
||||
"chromium_importer",
|
||||
"core",
|
||||
"macos_provider",
|
||||
"napi",
|
||||
@@ -68,14 +68,14 @@ tokio = "=1.45.0"
|
||||
tokio-stream = "=0.1.15"
|
||||
tokio-util = "=0.7.13"
|
||||
tracing = "=0.1.41"
|
||||
tracing-subscriber = { version = "=0.3.20", features = ["fmt", "env-filter"] }
|
||||
tracing-subscriber = { version = "=0.3.20", features = ["fmt", "env-filter", "tracing-log"] }
|
||||
typenum = "=1.18.0"
|
||||
uniffi = "=0.28.3"
|
||||
widestring = "=1.2.0"
|
||||
windows = "=0.61.1"
|
||||
windows-core = "=0.61.0"
|
||||
windows-future = "=0.2.0"
|
||||
windows-registry = "=0.5.3"
|
||||
windows-registry = "=0.6.1"
|
||||
zbus = "=5.11.0"
|
||||
zbus_polkit = "=5.0.0"
|
||||
zeroizing-alloc = "=0.1.0"
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
//! Cryptographic primitives used in the SDK
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
|
||||
use aes::cipher::{
|
||||
block_padding::Pkcs7, generic_array::GenericArray, typenum::U32, BlockDecryptMut, KeyIvInit,
|
||||
};
|
||||
|
||||
pub fn decrypt_aes256(iv: &[u8; 16], data: &[u8], key: GenericArray<u8, U32>) -> Result<Vec<u8>> {
|
||||
let iv = GenericArray::from_slice(iv);
|
||||
let mut data = data.to_vec();
|
||||
cbc::Decryptor::<aes::Aes256>::new(&key, iv)
|
||||
.decrypt_padded_mut::<Pkcs7>(&mut data)
|
||||
.map_err(|_| anyhow!("Failed to decrypt data"))?;
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use aes::cipher::{
|
||||
generic_array::{sequence::GenericSequence, GenericArray},
|
||||
ArrayLength,
|
||||
};
|
||||
use base64::{engine::general_purpose::STANDARD, Engine};
|
||||
|
||||
pub fn generate_vec(length: usize, offset: u8, increment: u8) -> Vec<u8> {
|
||||
(0..length).map(|i| offset + i as u8 * increment).collect()
|
||||
}
|
||||
pub fn generate_generic_array<N: ArrayLength<u8>>(
|
||||
offset: u8,
|
||||
increment: u8,
|
||||
) -> GenericArray<u8, N> {
|
||||
GenericArray::generate(|i| offset + i as u8 * increment)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decrypt_aes256() {
|
||||
let iv = generate_vec(16, 0, 1);
|
||||
let iv: &[u8; 16] = iv.as_slice().try_into().unwrap();
|
||||
let key = generate_generic_array(0, 1);
|
||||
let data: Vec<u8> = STANDARD.decode("ByUF8vhyX4ddU9gcooznwA==").unwrap();
|
||||
|
||||
let decrypted = super::decrypt_aes256(iv, &data, key).unwrap();
|
||||
|
||||
assert_eq!(String::from_utf8(decrypted).unwrap(), "EncryptMe!\u{6}\u{6}\u{6}\u{6}\u{6}\u{6}");
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
#[macro_use]
|
||||
extern crate napi_derive;
|
||||
|
||||
pub mod chromium;
|
||||
pub mod metadata;
|
||||
pub mod util;
|
||||
|
||||
pub use crate::chromium::platform::SUPPORTED_BROWSERS as PLATFORM_SUPPORTED_BROWSERS;
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "bitwarden_chromium_importer"
|
||||
name = "chromium_importer"
|
||||
edition = { workspace = true }
|
||||
license = { workspace = true }
|
||||
version = { workspace = true }
|
||||
@@ -14,8 +14,6 @@ base64 = { workspace = true }
|
||||
cbc = { workspace = true, features = ["alloc"] }
|
||||
hex = { workspace = true }
|
||||
homedir = { workspace = true }
|
||||
napi = { workspace = true }
|
||||
napi-derive = { workspace = true }
|
||||
pbkdf2 = "=0.12.2"
|
||||
rand = { workspace = true }
|
||||
rusqlite = { version = "=0.37.0", features = ["bundled"] }
|
||||
@@ -36,4 +34,3 @@ oo7 = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
# Windows ABE Architecture
|
||||
# Chromium Direct Importer
|
||||
|
||||
## Overview
|
||||
A rust library that allows you to directly import credentials from Chromium-based browsers.
|
||||
|
||||
## Windows ABE Architecture
|
||||
|
||||
On Windows chrome has additional protection measurements which needs to be circumvented in order to
|
||||
get access to the passwords.
|
||||
|
||||
### Overview
|
||||
|
||||
The Windows Application Bound Encryption (ABE) consists of three main components that work together:
|
||||
|
||||
@@ -10,7 +17,7 @@ The Windows Application Bound Encryption (ABE) consists of three main components
|
||||
|
||||
_(The names of the binaries will be changed for the released product.)_
|
||||
|
||||
## The goal
|
||||
### The goal
|
||||
|
||||
The goal of this subsystem is to decrypt the master encryption key with which the login information
|
||||
is encrypted on the local system in Windows. This applies to the most recent versions of Chrome and
|
||||
@@ -24,7 +31,7 @@ Protection API at the system level on top of that. This triply encrypted key is
|
||||
|
||||
The next paragraphs describe what is done at each level to decrypt the key.
|
||||
|
||||
## 1. Client library
|
||||
### 1. Client library
|
||||
|
||||
This is a Rust module that is part of the Chromium importer. It only compiles and runs on Windows
|
||||
(see `abe.rs` and `abe_config.rs`). Its main task is to launch `admin.exe` with elevated privileges
|
||||
@@ -52,7 +59,7 @@ admin.exe --service-exe "c:\temp\service.exe" --encrypted "QVBQQgEAAADQjJ3fARXRE
|
||||
|
||||
**At this point, the user must permit the action to be performed on the UAC screen.**
|
||||
|
||||
## 2. Admin executable
|
||||
### 2. Admin executable
|
||||
|
||||
This executable receives the full path of `service.exe` and the data to be decrypted.
|
||||
|
||||
@@ -67,7 +74,7 @@ is sent to the named pipe server created by the user. The user responds with `ok
|
||||
|
||||
After that, the executable stops and uninstalls the service and then exits.
|
||||
|
||||
## 3. System service
|
||||
### 3. System service
|
||||
|
||||
The service starts and creates a named pipe server for communication between `admin.exe` and the
|
||||
system service. Please note that it is not possible to communicate between the user and the system
|
||||
@@ -83,7 +90,7 @@ removed from the system. Even though we send only one request, the service is de
|
||||
many clients with as many messages as needed and could be installed on the system permanently if
|
||||
necessary.
|
||||
|
||||
## 4. Back to client library
|
||||
### 4. Back to client library
|
||||
|
||||
The decrypted base64-encoded string comes back from the admin executable to the named pipe server at
|
||||
the user level. At this point, it has been decrypted only once at the system level.
|
||||
@@ -99,7 +106,7 @@ itself), it's either AES-256-GCM or ChaCha20Poly1305 encryption scheme. The deta
|
||||
After all of these steps, we have the master key which can be used to decrypt the password
|
||||
information stored in the local database.
|
||||
|
||||
## Summary
|
||||
### Summary
|
||||
|
||||
The Windows ABE decryption process involves a three-tier architecture with named pipe communication:
|
||||
|
||||
@@ -7,11 +7,9 @@ use hex::decode;
|
||||
use homedir::my_home;
|
||||
use rusqlite::{params, Connection};
|
||||
|
||||
// Platform-specific code
|
||||
#[cfg_attr(target_os = "linux", path = "linux.rs")]
|
||||
#[cfg_attr(target_os = "windows", path = "windows.rs")]
|
||||
#[cfg_attr(target_os = "macos", path = "macos.rs")]
|
||||
pub mod platform;
|
||||
mod platform;
|
||||
|
||||
pub(crate) use platform::SUPPORTED_BROWSERS as PLATFORM_SUPPORTED_BROWSERS;
|
||||
|
||||
//
|
||||
// Public API
|
||||
@@ -22,10 +20,7 @@ pub struct ProfileInfo {
|
||||
pub name: String,
|
||||
pub folder: String,
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub account_name: Option<String>,
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub account_email: Option<String>,
|
||||
}
|
||||
|
||||
@@ -113,12 +108,12 @@ pub async fn import_logins(
|
||||
//
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct BrowserConfig {
|
||||
pub(crate) struct BrowserConfig {
|
||||
pub name: &'static str,
|
||||
pub data_dir: &'static str,
|
||||
}
|
||||
|
||||
pub static SUPPORTED_BROWSER_MAP: LazyLock<
|
||||
pub(crate) static SUPPORTED_BROWSER_MAP: LazyLock<
|
||||
std::collections::HashMap<&'static str, &'static BrowserConfig>,
|
||||
> = LazyLock::new(|| {
|
||||
platform::SUPPORTED_BROWSERS
|
||||
@@ -140,12 +135,12 @@ fn get_browser_data_dir(config: &BrowserConfig) -> Result<PathBuf> {
|
||||
//
|
||||
|
||||
#[async_trait]
|
||||
pub trait CryptoService: Send {
|
||||
pub(crate) trait CryptoService: Send {
|
||||
async fn decrypt_to_string(&mut self, encrypted: &[u8]) -> Result<String>;
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Clone)]
|
||||
pub struct LocalState {
|
||||
pub(crate) struct LocalState {
|
||||
profile: AllProfiles,
|
||||
#[allow(dead_code)]
|
||||
os_crypt: Option<OsCrypt>,
|
||||
@@ -198,16 +193,17 @@ fn load_local_state(browser_dir: &Path) -> Result<LocalState> {
|
||||
}
|
||||
|
||||
fn get_profile_info(local_state: &LocalState) -> Vec<ProfileInfo> {
|
||||
let mut profile_infos = Vec::new();
|
||||
for (name, info) in local_state.profile.info_cache.iter() {
|
||||
profile_infos.push(ProfileInfo {
|
||||
local_state
|
||||
.profile
|
||||
.info_cache
|
||||
.iter()
|
||||
.map(|(name, info)| ProfileInfo {
|
||||
name: info.name.clone(),
|
||||
folder: name.clone(),
|
||||
account_name: info.gaia_name.clone(),
|
||||
account_email: info.user_name.clone(),
|
||||
});
|
||||
}
|
||||
profile_infos
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
struct EncryptedLogin {
|
||||
@@ -264,17 +260,16 @@ fn hex_to_bytes(hex: &str) -> Vec<u8> {
|
||||
decode(hex).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn does_table_exist(conn: &Connection, table_name: &str) -> Result<bool, rusqlite::Error> {
|
||||
let mut stmt = conn.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?1")?;
|
||||
let exists = stmt.exists(params![table_name])?;
|
||||
Ok(exists)
|
||||
fn table_exist(conn: &Connection, table_name: &str) -> Result<bool, rusqlite::Error> {
|
||||
conn.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?1")?
|
||||
.exists(params![table_name])
|
||||
}
|
||||
|
||||
fn query_logins(db_path: &str) -> Result<Vec<EncryptedLogin>, rusqlite::Error> {
|
||||
let conn = Connection::open(db_path)?;
|
||||
|
||||
let have_logins = does_table_exist(&conn, "logins")?;
|
||||
let have_password_notes = does_table_exist(&conn, "password_notes")?;
|
||||
let have_logins = table_exist(&conn, "logins")?;
|
||||
let have_password_notes = table_exist(&conn, "password_notes")?;
|
||||
if !have_logins || !have_password_notes {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
@@ -308,10 +303,7 @@ fn query_logins(db_path: &str) -> Result<Vec<EncryptedLogin>, rusqlite::Error> {
|
||||
})
|
||||
})?;
|
||||
|
||||
let mut logins = Vec::new();
|
||||
for login in logins_iter {
|
||||
logins.push(login?);
|
||||
}
|
||||
let logins = logins_iter.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(logins)
|
||||
}
|
||||
@@ -13,7 +13,7 @@ use crate::util;
|
||||
//
|
||||
|
||||
// TODO: It's possible that there might be multiple possible data directories, depending on the installation method (e.g., snap, flatpak, etc.).
|
||||
pub const SUPPORTED_BROWSERS: [BrowserConfig; 4] = [
|
||||
pub(crate) const SUPPORTED_BROWSERS: &[BrowserConfig] = &[
|
||||
BrowserConfig {
|
||||
name: "Chrome",
|
||||
data_dir: ".config/google-chrome",
|
||||
@@ -32,7 +32,7 @@ pub const SUPPORTED_BROWSERS: [BrowserConfig; 4] = [
|
||||
},
|
||||
];
|
||||
|
||||
pub fn get_crypto_service(
|
||||
pub(crate) fn get_crypto_service(
|
||||
browser_name: &String,
|
||||
_local_state: &LocalState,
|
||||
) -> Result<Box<dyn CryptoService>> {
|
||||
@@ -10,7 +10,7 @@ use crate::util;
|
||||
// Public API
|
||||
//
|
||||
|
||||
pub const SUPPORTED_BROWSERS: [BrowserConfig; 7] = [
|
||||
pub(crate) const SUPPORTED_BROWSERS: &[BrowserConfig] = &[
|
||||
BrowserConfig {
|
||||
name: "Chrome",
|
||||
data_dir: "Library/Application Support/Google/Chrome",
|
||||
@@ -41,7 +41,7 @@ pub const SUPPORTED_BROWSERS: [BrowserConfig; 7] = [
|
||||
},
|
||||
];
|
||||
|
||||
pub fn get_crypto_service(
|
||||
pub(crate) fn get_crypto_service(
|
||||
browser_name: &String,
|
||||
_local_state: &LocalState,
|
||||
) -> Result<Box<dyn CryptoService>> {
|
||||
@@ -0,0 +1,7 @@
|
||||
// Platform-specific code
|
||||
#[cfg_attr(target_os = "linux", path = "linux.rs")]
|
||||
#[cfg_attr(target_os = "windows", path = "windows.rs")]
|
||||
#[cfg_attr(target_os = "macos", path = "macos.rs")]
|
||||
mod native;
|
||||
|
||||
pub(crate) use native::*;
|
||||
@@ -15,8 +15,7 @@ use crate::util;
|
||||
// Public API
|
||||
//
|
||||
|
||||
// IMPORTANT adjust array size when enabling / disabling chromium importers here
|
||||
pub const SUPPORTED_BROWSERS: [BrowserConfig; 6] = [
|
||||
pub(crate) const SUPPORTED_BROWSERS: &[BrowserConfig] = &[
|
||||
BrowserConfig {
|
||||
name: "Brave",
|
||||
data_dir: "AppData/Local/BraveSoftware/Brave-Browser/User Data",
|
||||
@@ -43,7 +42,7 @@ pub const SUPPORTED_BROWSERS: [BrowserConfig; 6] = [
|
||||
},
|
||||
];
|
||||
|
||||
pub fn get_crypto_service(
|
||||
pub(crate) fn get_crypto_service(
|
||||
_browser_name: &str,
|
||||
local_state: &LocalState,
|
||||
) -> Result<Box<dyn CryptoService>> {
|
||||
5
apps/desktop/desktop_native/chromium_importer/src/lib.rs
Normal file
5
apps/desktop/desktop_native/chromium_importer/src/lib.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
pub mod chromium;
|
||||
pub mod metadata;
|
||||
mod util;
|
||||
@@ -1,8 +1,7 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use crate::{chromium::InstalledBrowserRetriever, PLATFORM_SUPPORTED_BROWSERS};
|
||||
use crate::chromium::{InstalledBrowserRetriever, PLATFORM_SUPPORTED_BROWSERS};
|
||||
|
||||
#[napi(object)]
|
||||
/// Mechanisms that load data into the importer
|
||||
pub struct NativeImporterMetadata {
|
||||
/// Identifies the importer
|
||||
@@ -24,7 +23,7 @@ pub fn get_supported_importers<T: InstalledBrowserRetriever>(
|
||||
// Check for installed browsers
|
||||
let installed_browsers = T::get_installed_browsers().unwrap_or_default();
|
||||
|
||||
const IMPORTERS: [(&str, &str); 6] = [
|
||||
const IMPORTERS: &[(&str, &str)] = &[
|
||||
("chromecsv", "Chrome"),
|
||||
("chromiumcsv", "Chromium"),
|
||||
("bravecsv", "Brave"),
|
||||
@@ -57,9 +56,7 @@ pub fn get_supported_importers<T: InstalledBrowserRetriever>(
|
||||
map
|
||||
}
|
||||
|
||||
/*
|
||||
Tests are cfg-gated based upon OS, and must be compiled/run on each OS for full coverage
|
||||
*/
|
||||
// Tests are cfg-gated based upon OS, and must be compiled/run on each OS for full coverage
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -1,9 +1,6 @@
|
||||
use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit};
|
||||
use anyhow::{anyhow, Result};
|
||||
use pbkdf2::{hmac::Hmac, pbkdf2};
|
||||
use sha1::Sha1;
|
||||
|
||||
pub fn split_encrypted_string(encrypted: &[u8]) -> Result<(&str, &[u8])> {
|
||||
fn split_encrypted_string(encrypted: &[u8]) -> Result<(&str, &[u8])> {
|
||||
if encrypted.len() < 3 {
|
||||
return Err(anyhow!(
|
||||
"Corrupted entry: invalid encrypted string length, expected at least 3 bytes, got {}",
|
||||
@@ -15,7 +12,14 @@ pub fn split_encrypted_string(encrypted: &[u8]) -> Result<(&str, &[u8])> {
|
||||
Ok((std::str::from_utf8(version)?, password))
|
||||
}
|
||||
|
||||
pub fn split_encrypted_string_and_validate<'a>(
|
||||
/// A Chromium password consists of three parts:
|
||||
/// - Version (3 bytes): "v10", "v11", etc.
|
||||
/// - Cipher text (chunks of 16 bytes)
|
||||
/// - Padding (1-15 bytes)
|
||||
///
|
||||
/// This function splits the encrypted byte slice into version and cipher text.
|
||||
/// Padding is included and handled by the underlying cryptographic library.
|
||||
pub(crate) fn split_encrypted_string_and_validate<'a>(
|
||||
encrypted: &'a [u8],
|
||||
supported_versions: &[&str],
|
||||
) -> Result<(&'a str, &'a [u8])> {
|
||||
@@ -27,15 +31,22 @@ pub fn split_encrypted_string_and_validate<'a>(
|
||||
Ok((version, password))
|
||||
}
|
||||
|
||||
pub fn decrypt_aes_128_cbc(key: &[u8], iv: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>> {
|
||||
let decryptor = cbc::Decryptor::<aes::Aes128>::new_from_slices(key, iv)?;
|
||||
let plaintext: Vec<u8> = decryptor
|
||||
/// Decrypt using AES-128 in CBC mode.
|
||||
#[cfg(any(target_os = "linux", target_os = "macos", test))]
|
||||
pub(crate) fn decrypt_aes_128_cbc(key: &[u8], iv: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>> {
|
||||
use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit};
|
||||
|
||||
cbc::Decryptor::<aes::Aes128>::new_from_slices(key, iv)?
|
||||
.decrypt_padded_vec_mut::<Pkcs7>(ciphertext)
|
||||
.map_err(|e| anyhow!("Failed to decrypt: {}", e))?;
|
||||
Ok(plaintext)
|
||||
.map_err(|e| anyhow!("Failed to decrypt: {}", e))
|
||||
}
|
||||
|
||||
pub fn derive_saltysalt(password: &[u8], iterations: u32) -> Result<Vec<u8>> {
|
||||
/// Derives a PBKDF2 key from the static "saltysalt" salt with the given password and iteration count.
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
pub(crate) fn derive_saltysalt(password: &[u8], iterations: u32) -> Result<Vec<u8>> {
|
||||
use pbkdf2::{hmac::Hmac, pbkdf2};
|
||||
use sha1::Sha1;
|
||||
|
||||
let mut key = vec![0u8; 16];
|
||||
pbkdf2::<Hmac<Sha1>>(password, b"saltysalt", iterations, &mut key)
|
||||
.map_err(|e| anyhow!("Failed to derive master key: {}", e))?;
|
||||
@@ -44,16 +55,6 @@ pub fn derive_saltysalt(password: &[u8], iterations: u32) -> Result<Vec<u8>> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
pub fn generate_vec(length: usize, offset: u8, increment: u8) -> Vec<u8> {
|
||||
(0..length).map(|i| offset + i as u8 * increment).collect()
|
||||
}
|
||||
pub fn generate_generic_array<N: ArrayLength<u8>>(
|
||||
offset: u8,
|
||||
increment: u8,
|
||||
) -> GenericArray<u8, N> {
|
||||
GenericArray::generate(|i| offset + i as u8 * increment)
|
||||
}
|
||||
|
||||
use aes::cipher::{
|
||||
block_padding::Pkcs7,
|
||||
generic_array::{sequence::GenericSequence, GenericArray},
|
||||
@@ -64,6 +65,17 @@ mod tests {
|
||||
const LENGTH10: usize = 10;
|
||||
const LENGTH0: usize = 0;
|
||||
|
||||
fn generate_vec(length: usize, offset: u8, increment: u8) -> Vec<u8> {
|
||||
(0..length).map(|i| offset + i as u8 * increment).collect()
|
||||
}
|
||||
|
||||
fn generate_generic_array<N: ArrayLength<u8>>(
|
||||
offset: u8,
|
||||
increment: u8,
|
||||
) -> GenericArray<u8, N> {
|
||||
GenericArray::generate(|i| offset + i as u8 * increment)
|
||||
}
|
||||
|
||||
fn run_split_encrypted_string_test<'a, const N: usize>(
|
||||
successfully_split: bool,
|
||||
plaintext_to_encrypt: &'a str,
|
||||
141
apps/desktop/desktop_native/core/src/biometric_v2/linux.rs
Normal file
141
apps/desktop/desktop_native/core/src/biometric_v2/linux.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
//! This file implements Polkit based system unlock.
|
||||
//!
|
||||
//! # Security
|
||||
//! This section describes the assumed security model and security guarantees achieved. In the required security
|
||||
//! guarantee is that a locked vault - a running app - cannot be unlocked when the device (user-space)
|
||||
//! is compromised in this state.
|
||||
//!
|
||||
//! When first unlocking the app, the app sends the user-key to this module, which holds it in secure memory,
|
||||
//! protected by memfd_secret. This makes it inaccessible to other processes, even if they compromise root, a kernel compromise
|
||||
//! has circumventable best-effort protections. While the app is running this key is held in memory, even if locked.
|
||||
//! When unlocking, the app will prompt the user via `polkit` to get a yes/no decision on whether to release the key to the app.
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{debug, warn};
|
||||
use zbus::Connection;
|
||||
use zbus_polkit::policykit1::{AuthorityProxy, CheckAuthorizationFlags, Subject};
|
||||
|
||||
use crate::secure_memory::*;
|
||||
|
||||
pub struct BiometricLockSystem {
|
||||
// The userkeys that are held in memory MUST be protected from memory dumping attacks, to ensure
|
||||
// locked vaults cannot be unlocked
|
||||
secure_memory: Arc<Mutex<crate::secure_memory::encrypted_memory_store::EncryptedMemoryStore>>,
|
||||
}
|
||||
|
||||
impl BiometricLockSystem {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
secure_memory: Arc::new(Mutex::new(
|
||||
crate::secure_memory::encrypted_memory_store::EncryptedMemoryStore::new(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for BiometricLockSystem {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl super::BiometricTrait for BiometricLockSystem {
|
||||
async fn authenticate(&self, _hwnd: Vec<u8>, _message: String) -> Result<bool> {
|
||||
polkit_authenticate_bitwarden_policy().await
|
||||
}
|
||||
|
||||
async fn authenticate_available(&self) -> Result<bool> {
|
||||
polkit_is_bitwarden_policy_available().await
|
||||
}
|
||||
|
||||
async fn enroll_persistent(&self, _user_id: &str, _key: &[u8]) -> Result<()> {
|
||||
// Not implemented
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn provide_key(&self, user_id: &str, key: &[u8]) {
|
||||
self.secure_memory
|
||||
.lock()
|
||||
.await
|
||||
.put(user_id.to_string(), key);
|
||||
}
|
||||
|
||||
async fn unlock(&self, user_id: &str, _hwnd: Vec<u8>) -> Result<Vec<u8>> {
|
||||
if !polkit_authenticate_bitwarden_policy().await? {
|
||||
return Err(anyhow!("Authentication failed"));
|
||||
}
|
||||
|
||||
self.secure_memory
|
||||
.lock()
|
||||
.await
|
||||
.get(user_id)
|
||||
.ok_or(anyhow!("No key found"))
|
||||
}
|
||||
|
||||
async fn unlock_available(&self, user_id: &str) -> Result<bool> {
|
||||
Ok(self.secure_memory.lock().await.has(user_id))
|
||||
}
|
||||
|
||||
async fn has_persistent(&self, _user_id: &str) -> Result<bool> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
async fn unenroll(&self, user_id: &str) -> Result<(), anyhow::Error> {
|
||||
self.secure_memory.lock().await.remove(user_id);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform a polkit authorization against the bitwarden unlock policy. Note: This relies on no custom
|
||||
/// rules in the system skipping the authorization check, in which case this counts as UV / authentication.
|
||||
async fn polkit_authenticate_bitwarden_policy() -> Result<bool> {
|
||||
debug!("[Polkit] Authenticating / performing UV");
|
||||
|
||||
let connection = Connection::system().await?;
|
||||
let proxy = AuthorityProxy::new(&connection).await?;
|
||||
let subject = Subject::new_for_owner(std::process::id(), None, None)?;
|
||||
let details = std::collections::HashMap::new();
|
||||
let authorization_result = proxy
|
||||
.check_authorization(
|
||||
&subject,
|
||||
"com.bitwarden.Bitwarden.unlock",
|
||||
&details,
|
||||
CheckAuthorizationFlags::AllowUserInteraction.into(),
|
||||
"",
|
||||
)
|
||||
.await;
|
||||
|
||||
match authorization_result {
|
||||
Ok(result) => Ok(result.is_authorized),
|
||||
Err(e) => {
|
||||
warn!("[Polkit] Error performing authentication: {:?}", e);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn polkit_is_bitwarden_policy_available() -> Result<bool> {
|
||||
let connection = Connection::system().await?;
|
||||
let proxy = AuthorityProxy::new(&connection).await?;
|
||||
let actions = proxy.enumerate_actions("en").await?;
|
||||
for action in actions {
|
||||
if action.action_id == "com.bitwarden.Bitwarden.unlock" {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_polkit_authenticate() {
|
||||
let result = polkit_authenticate_bitwarden_policy().await;
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::Result;
|
||||
|
||||
#[allow(clippy::module_inception)]
|
||||
#[cfg_attr(target_os = "linux", path = "unimplemented.rs")]
|
||||
#[cfg_attr(target_os = "linux", path = "linux.rs")]
|
||||
#[cfg_attr(target_os = "macos", path = "unimplemented.rs")]
|
||||
#[cfg_attr(target_os = "windows", path = "windows.rs")]
|
||||
mod biometric_v2;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) mod dpapi;
|
||||
|
||||
mod encrypted_memory_store;
|
||||
pub(crate) mod encrypted_memory_store;
|
||||
mod secure_key;
|
||||
|
||||
/// The secure memory store provides an ephemeral key-value store for sensitive data.
|
||||
|
||||
@@ -17,7 +17,7 @@ manual_test = []
|
||||
anyhow = { workspace = true }
|
||||
autotype = { path = "../autotype" }
|
||||
base64 = { workspace = true }
|
||||
bitwarden_chromium_importer = { path = "../bitwarden_chromium_importer" }
|
||||
chromium_importer = { path = "../chromium_importer" }
|
||||
desktop_core = { path = "../core" }
|
||||
hex = { workspace = true }
|
||||
napi = { workspace = true, features = ["async"] }
|
||||
|
||||
15
apps/desktop/desktop_native/napi/index.d.ts
vendored
15
apps/desktop/desktop_native/napi/index.d.ts
vendored
@@ -3,15 +3,6 @@
|
||||
|
||||
/* auto-generated by NAPI-RS */
|
||||
|
||||
/** Mechanisms that load data into the importer */
|
||||
export interface NativeImporterMetadata {
|
||||
/** Identifies the importer */
|
||||
id: string
|
||||
/** Describes the strategies used to obtain imported data */
|
||||
loaders: Array<string>
|
||||
/** Identifies the instructions for the importer */
|
||||
instructions: string
|
||||
}
|
||||
export declare namespace passwords {
|
||||
/** The error message returned when a password is not found during retrieval or deletion. */
|
||||
export const PASSWORD_NOT_FOUND: string
|
||||
@@ -249,9 +240,13 @@ export declare namespace chromium_importer {
|
||||
login?: Login
|
||||
failure?: LoginImportFailure
|
||||
}
|
||||
export interface NativeImporterMetadata {
|
||||
id: string
|
||||
loaders: Array<string>
|
||||
instructions: string
|
||||
}
|
||||
/** Returns OS aware metadata describing supported Chromium based importers as a JSON string. */
|
||||
export function getMetadata(): Record<string, NativeImporterMetadata>
|
||||
export function getInstalledBrowsers(): Array<string>
|
||||
export function getAvailableProfiles(browser: string): Array<ProfileInfo>
|
||||
export function importLogins(browser: string, profileId: string): Promise<Array<LoginImportResult>>
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user