mirror of
https://github.com/bitwarden/browser
synced 2026-02-08 20:50:28 +00:00
Merge branch 'main' of https://github.com/bitwarden/clients into PM-24304
This commit is contained in:
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -143,6 +143,7 @@ apps/desktop/macos/autofill-extension @bitwarden/team-autofill-dev
|
||||
apps/desktop/src/app/components/fido2placeholder.component.ts @bitwarden/team-autofill-dev
|
||||
apps/desktop/desktop_native/windows_plugin_authenticator @bitwarden/team-autofill-dev
|
||||
apps/desktop/desktop_native/autotype @bitwarden/team-autofill-dev
|
||||
.github/workflows/test-browser-interactions.yml @bitwarden/team-autofill-dev
|
||||
# DuckDuckGo integration
|
||||
apps/desktop/native-messaging-test-runner @bitwarden/team-autofill-dev
|
||||
apps/desktop/src/services/duckduckgo-message-handler.service.ts @bitwarden/team-autofill-dev
|
||||
|
||||
2
.github/workflows/build-browser.yml
vendored
2
.github/workflows/build-browser.yml
vendored
@@ -232,7 +232,7 @@ jobs:
|
||||
npm --version
|
||||
|
||||
- name: Download browser source
|
||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
name: browser-source-${{ env._BUILD_NUMBER }}.zip
|
||||
|
||||
|
||||
2
.github/workflows/build-cli.yml
vendored
2
.github/workflows/build-cli.yml
vendored
@@ -499,7 +499,7 @@ jobs:
|
||||
echo "BW Package Version: $_PACKAGE_VERSION"
|
||||
|
||||
- name: Get bw linux cli
|
||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
name: bw-linux-${{ env._PACKAGE_VERSION }}.zip
|
||||
path: apps/cli/dist/snap
|
||||
|
||||
4
.github/workflows/build-desktop.yml
vendored
4
.github/workflows/build-desktop.yml
vendored
@@ -1079,7 +1079,7 @@ jobs:
|
||||
run: npm run build
|
||||
|
||||
- name: Download Browser artifact
|
||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
path: ${{ github.workspace }}/browser-build-artifacts
|
||||
|
||||
@@ -1344,7 +1344,7 @@ jobs:
|
||||
run: npm run build
|
||||
|
||||
- name: Download Browser artifact
|
||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
path: ${{ github.workspace }}/browser-build-artifacts
|
||||
|
||||
|
||||
2
.github/workflows/release-desktop-beta.yml
vendored
2
.github/workflows/release-desktop-beta.yml
vendored
@@ -1035,7 +1035,7 @@ jobs:
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
path: apps/desktop/artifacts
|
||||
|
||||
|
||||
83
.github/workflows/test-browser-interactions.yml
vendored
Normal file
83
.github/workflows/test-browser-interactions.yml
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
name: Autofill BIT checks
|
||||
run-name: Autofill BIT checks on ${{ github.event.workflow_run.head_branch }} build
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build Browser"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
check-files:
|
||||
name: Check files
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check for job requirements
|
||||
if: ${{ !github.event.workflow_run.pull_requests || !github.event.workflow_run.head_branch }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh run cancel ${{ github.run_id }}
|
||||
gh run watch ${{ github.run_id }}
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
with:
|
||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Get Azure Key Vault secrets
|
||||
id: get-kv-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: gh-org-bitwarden
|
||||
secrets: "BW-GHAPP-ID,BW-GHAPP-KEY"
|
||||
|
||||
- name: Log out from Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@30bf6253fa41bdc8d1501d202ad15287582246b4 # v2.0.3
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
|
||||
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
|
||||
owner: bitwarden
|
||||
repositories: browser-interactions-testing
|
||||
permission-actions: write
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
with:
|
||||
list-files: shell
|
||||
ref: ${{ github.event.workflow_run.head_branch }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
filters: |
|
||||
monitored:
|
||||
- 'apps/browser/src/autofill/**'
|
||||
- 'apps/browser/src/background/**'
|
||||
- 'apps/browser/src/platform/services/browser-script-injector.service.ts'
|
||||
|
||||
- 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
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
repository: "bitwarden/browser-interactions-testing"
|
||||
event-type: trigger-bit-tests
|
||||
client-payload: |-
|
||||
{
|
||||
"origin_issue": ${{ github.event.workflow_run.pull_requests[0].number }},
|
||||
"origin_branch": "${{ github.event.workflow_run.pull_requests[0].head.ref }}"
|
||||
}
|
||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -170,13 +170,13 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Download jest coverage
|
||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
name: jest-coverage
|
||||
path: ./
|
||||
|
||||
- name: Download rust coverage
|
||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
name: rust-coverage
|
||||
path: ./apps/desktop/desktop_native
|
||||
|
||||
@@ -338,7 +338,7 @@
|
||||
"message": "Pokračovat na bitwarden.com?"
|
||||
},
|
||||
"bitwardenForBusiness": {
|
||||
"message": "Bitwarden pro byznys"
|
||||
"message": "Bitwarden pro podnikání"
|
||||
},
|
||||
"bitwardenAuthenticator": {
|
||||
"message": "Autentifikátor Bitwarden"
|
||||
@@ -398,7 +398,7 @@
|
||||
"message": "Název složky"
|
||||
},
|
||||
"folderHintText": {
|
||||
"message": "Vnořit složku přidáním názvu nadřazené složky následovaného znakem \"/\". Příklad: Sociální/Fóra"
|
||||
"message": "Složku vnoříte přidáním názvu nadřazené složky následovaného znakem \"/\". Příklad: Sociální/Fóra"
|
||||
},
|
||||
"noFoldersAdded": {
|
||||
"message": "Nebyly přidány žádné složky"
|
||||
@@ -407,7 +407,7 @@
|
||||
"message": "Vytvořte složky pro organizaci Vašich položek trezoru"
|
||||
},
|
||||
"deleteFolderPermanently": {
|
||||
"message": "Opravdu chcete trvale smazat tuto složku?"
|
||||
"message": "Opravdu chcete tuto složku trvale smazat?"
|
||||
},
|
||||
"deleteFolder": {
|
||||
"message": "Smazat složku"
|
||||
@@ -531,10 +531,10 @@
|
||||
"message": "Zahrnout číslice"
|
||||
},
|
||||
"minNumbers": {
|
||||
"message": "Minimální počet číslic"
|
||||
"message": "Minimálně číslic"
|
||||
},
|
||||
"minSpecial": {
|
||||
"message": "Minimální počet speciálních znaků"
|
||||
"message": "Minimálně speciálních znaků"
|
||||
},
|
||||
"avoidAmbiguous": {
|
||||
"message": "Nepoužívat zaměnitelné znaky",
|
||||
@@ -1016,7 +1016,7 @@
|
||||
"description": "This is the folder for uncategorized items"
|
||||
},
|
||||
"enableAddLoginNotification": {
|
||||
"message": "Ptát se na přidání přihlášení"
|
||||
"message": "Zeptat se na přidání přihlášení"
|
||||
},
|
||||
"vaultSaveOptionsTitle": {
|
||||
"message": "Uložit do voleb trezoru"
|
||||
@@ -1049,7 +1049,7 @@
|
||||
"message": "Klepněte na položky pro automatické vyplnění v zobrazení trezoru"
|
||||
},
|
||||
"clickToAutofill": {
|
||||
"message": "Klepněte na položky v návrhu automatického vyplňování pro vyplnění"
|
||||
"message": "Klepnout na položky v návrhu automatického vyplňování pro vyplnění"
|
||||
},
|
||||
"clearClipboard": {
|
||||
"message": "Vymazat schránku",
|
||||
@@ -1305,7 +1305,7 @@
|
||||
"message": "Sdílené"
|
||||
},
|
||||
"bitwardenForBusinessPageDesc": {
|
||||
"message": "Bitwarden pro bvyznys Vám umožňuje sdílet položky v trezoru s ostatními prostřednictvím organizace. Více informací naleznete na bitwarden.com."
|
||||
"message": "Bitwarden pro podnikání Vám umožňuje sdílet položky v trezoru s ostatními prostřednictvím organizace. Více informací naleznete na bitwarden.com."
|
||||
},
|
||||
"moveToOrganization": {
|
||||
"message": "Přesunout do organizace"
|
||||
@@ -1660,7 +1660,7 @@
|
||||
"description": "Overlay appearance select option for showing the field on click of the overlay icon"
|
||||
},
|
||||
"enableAutoFillOnPageLoadSectionTitle": {
|
||||
"message": "Automaticky vyplnit údaje při načtení stránky"
|
||||
"message": "Automatické vyplnění údajů při načtení stránky"
|
||||
},
|
||||
"enableAutoFillOnPageLoad": {
|
||||
"message": "Automaticky vyplnit údaje při načtení stránky"
|
||||
@@ -1678,7 +1678,7 @@
|
||||
"message": "Více informací o automatickém vyplňování"
|
||||
},
|
||||
"defaultAutoFillOnPageLoad": {
|
||||
"message": "Výchozí nastavení automatického vyplňování pro položky přihlášení"
|
||||
"message": "Výchozí nastavení autom. vyplňování"
|
||||
},
|
||||
"defaultAutoFillOnPageLoadDesc": {
|
||||
"message": "Můžete vypnout automatické vyplňování při načtení stránky pro jednotlivé přihlašovací položky v zobrazení pro úpravu položky."
|
||||
@@ -1690,10 +1690,10 @@
|
||||
"message": "Použít výchozí nastavení"
|
||||
},
|
||||
"autoFillOnPageLoadYes": {
|
||||
"message": "Automatické vyplnění při načtení stránky"
|
||||
"message": "Automatické vyplnění při načtení"
|
||||
},
|
||||
"autoFillOnPageLoadNo": {
|
||||
"message": "Nevyplňovat automaticky při načtení stránky"
|
||||
"message": "Nevyplňovat automaticky při načtení"
|
||||
},
|
||||
"commandOpenPopup": {
|
||||
"message": "Otevřít vyskakovací okno trezoru"
|
||||
@@ -1851,7 +1851,7 @@
|
||||
"message": "Slečna"
|
||||
},
|
||||
"dr": {
|
||||
"message": "MUDr."
|
||||
"message": "Dr."
|
||||
},
|
||||
"mx": {
|
||||
"message": "Neutrální"
|
||||
@@ -1875,7 +1875,7 @@
|
||||
"message": "Společnost"
|
||||
},
|
||||
"ssn": {
|
||||
"message": "Číslo sociálního pojištění"
|
||||
"message": "Rodné číslo"
|
||||
},
|
||||
"passportNumber": {
|
||||
"message": "Číslo cestovního pasu"
|
||||
@@ -2099,7 +2099,7 @@
|
||||
"message": "Nic k zobrazení"
|
||||
},
|
||||
"nothingGeneratedRecently": {
|
||||
"message": "Nedávno jste nic nevygenerovali"
|
||||
"message": "Ještě jste nic nevygenerovali"
|
||||
},
|
||||
"remove": {
|
||||
"message": "Odebrat"
|
||||
@@ -2233,7 +2233,7 @@
|
||||
"description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'"
|
||||
},
|
||||
"useGeneratorHelpTextPartTwo": {
|
||||
"message": "pro vytvoření silného jedinečného hesla",
|
||||
"message": "pro vytvoření silného jedinečného hesla.",
|
||||
"description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'"
|
||||
},
|
||||
"vaultCustomization": {
|
||||
@@ -2685,7 +2685,7 @@
|
||||
"message": "Změny v zablokovaných doménách byly uloženy"
|
||||
},
|
||||
"excludedDomainsSavedSuccess": {
|
||||
"message": "Vyloučené změny domény byly uloženy"
|
||||
"message": "Změny vyloučené domény byly uloženy"
|
||||
},
|
||||
"limitSendViews": {
|
||||
"message": "Omezit zobrazení"
|
||||
@@ -2911,7 +2911,7 @@
|
||||
"message": "Došlo k chybě při ukládání datumu smzání a vypršení platnosti."
|
||||
},
|
||||
"hideYourEmail": {
|
||||
"message": "Skryje Vaši e-mailovou adresu před zobrazením."
|
||||
"message": "Skrýt Vaši e-mailovou adresu"
|
||||
},
|
||||
"passwordPrompt": {
|
||||
"message": "Zeptat se znovu na hlavní heslo"
|
||||
@@ -3228,7 +3228,7 @@
|
||||
"description": "Labels the domain name email forwarder service option"
|
||||
},
|
||||
"forwarderDomainNameHint": {
|
||||
"message": "Vyberte doménu, která je podporována vybranou službou",
|
||||
"message": "Vyberte doménu, která je podporována vybranou službou.",
|
||||
"description": "Guidance provided for email forwarding services that support multiple email domains."
|
||||
},
|
||||
"forwarderError": {
|
||||
@@ -4132,7 +4132,7 @@
|
||||
"message": "Zvolte sbírku"
|
||||
},
|
||||
"importTargetHint": {
|
||||
"message": "Pokud chcete obsah importovaného souboru přesunout do složky $DESTINATION$, vyberte tuto volbu",
|
||||
"message": "Pokud chcete obsah importovaného souboru přesunout do: \"$DESTINATION$\", vyberte tuto volbu",
|
||||
"description": "Located as a hint under the import target. Will be appended by either folder or collection, depending if the user is importing into an individual or an organizational vault.",
|
||||
"placeholders": {
|
||||
"destination": {
|
||||
@@ -4407,7 +4407,7 @@
|
||||
"description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page"
|
||||
},
|
||||
"confirmContinueToHelpCenterPasswordManagementContent": {
|
||||
"message": "Změňte nastavení automatického vyplňování a správy hesel.",
|
||||
"message": "Změna nastavení automatického vyplňování a nastavení správy hesel.",
|
||||
"description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser password management settings"
|
||||
},
|
||||
"confirmContinueToHelpCenterKeyboardShortcutsContent": {
|
||||
@@ -4415,7 +4415,7 @@
|
||||
"description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser keyboard shortcut settings"
|
||||
},
|
||||
"confirmContinueToBrowserPasswordManagementSettingsContent": {
|
||||
"message": "Změňte nastavení automatického vyplňování a správy hesel.",
|
||||
"message": "Změna nastavení automatického vyplňování a nastavení správy hesel.",
|
||||
"description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page"
|
||||
},
|
||||
"confirmContinueToBrowserKeyboardShortcutSettingsContent": {
|
||||
@@ -4945,13 +4945,13 @@
|
||||
"message": "Popis pole"
|
||||
},
|
||||
"textHelpText": {
|
||||
"message": "Použijte textová pole pro data jako bezpečnostní otázky"
|
||||
"message": "Použijte textová pole pro data (jako např. bezpečnostní otázky)."
|
||||
},
|
||||
"hiddenHelpText": {
|
||||
"message": "Použijte skrytá pole pro citlivá data, jako je heslo"
|
||||
"message": "Použijte skrytá pole pro citlivá data, jako je heslo."
|
||||
},
|
||||
"checkBoxHelpText": {
|
||||
"message": "Použijte zaškrtávací políčka, pokud chcete automaticky vyplnit zaškrtávací políčko formuláře (např. pro zapamatování e-mailu)"
|
||||
"message": "Použijte zaškrtávací políčka, pokud chcete automaticky zvolit zaškrtávací políčko formuláře (např. pro zapamatování e-mailu)."
|
||||
},
|
||||
"linkedHelpText": {
|
||||
"message": "Použijte propojené pole, pokud máte problémy s automatickým vyplňováním na konkrétní webové stránce."
|
||||
@@ -5121,7 +5121,7 @@
|
||||
"message": "Zobrazit akce rychlé kopie v trezoru"
|
||||
},
|
||||
"systemDefault": {
|
||||
"message": "Systémový výchozí"
|
||||
"message": "Výchozí systémový"
|
||||
},
|
||||
"enterprisePolicyRequirementsApplied": {
|
||||
"message": "Na toto nastavení byly uplatněny požadavky podnikových zásad"
|
||||
@@ -5178,7 +5178,7 @@
|
||||
"message": "Položky, které smažete, se zde zobrazí a budou trvale smazány po 30 dnech."
|
||||
},
|
||||
"trashWarning": {
|
||||
"message": "Položky, které byly v koši déle než 30 dní, budou automaticky smazány."
|
||||
"message": "Položky, které byly v koši déle než 30 dnů, budou automaticky smazány."
|
||||
},
|
||||
"restore": {
|
||||
"message": "Obnovit"
|
||||
@@ -5394,10 +5394,10 @@
|
||||
"message": "Šířka rozšíření"
|
||||
},
|
||||
"wide": {
|
||||
"message": "Šířka"
|
||||
"message": "Široké"
|
||||
},
|
||||
"extraWide": {
|
||||
"message": "Extra široký"
|
||||
"message": "Extra široké"
|
||||
},
|
||||
"sshKeyWrongPassword": {
|
||||
"message": "Zadané heslo není správné."
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
"message": "Υπόδειξη κύριου κωδικού πρόσβασης (προαιρετικό)"
|
||||
},
|
||||
"passwordStrengthScore": {
|
||||
"message": "Password strength score $SCORE$",
|
||||
"message": "Βαθμολογία ισχύς κωδικού πρόσβασης $SCORE$",
|
||||
"placeholders": {
|
||||
"score": {
|
||||
"content": "$1",
|
||||
@@ -468,13 +468,13 @@
|
||||
"message": "Ο κωδικός δημιουργήθηκε"
|
||||
},
|
||||
"passphraseGenerated": {
|
||||
"message": "Passphrase generated"
|
||||
"message": "Το συνθηματικό δημιουργήθηκε"
|
||||
},
|
||||
"usernameGenerated": {
|
||||
"message": "Username generated"
|
||||
"message": "Το όνομα χρήστη δημιουργήθηκε"
|
||||
},
|
||||
"emailGenerated": {
|
||||
"message": "Email generated"
|
||||
"message": "Το email δημιουργήθηκε"
|
||||
},
|
||||
"regeneratePassword": {
|
||||
"message": "Επαναδημιουργία κωδικού πρόσβασης"
|
||||
@@ -548,7 +548,7 @@
|
||||
"message": "Αναζήτηση στο vault"
|
||||
},
|
||||
"resetSearch": {
|
||||
"message": "Reset search"
|
||||
"message": "Επαναφορά αναζήτησης"
|
||||
},
|
||||
"edit": {
|
||||
"message": "Επεξεργασία"
|
||||
@@ -659,10 +659,10 @@
|
||||
"message": "Το πρόγραμμα περιήγησης ιστού δεν υποστηρίζει εύκολη αντιγραφή πρόχειρου. Αντιγράψτε το με το χέρι αντ'αυτού."
|
||||
},
|
||||
"verifyYourIdentity": {
|
||||
"message": "Verify your identity"
|
||||
"message": "Επαλήθευση ταυτότητας"
|
||||
},
|
||||
"weDontRecognizeThisDevice": {
|
||||
"message": "We don't recognize this device. Enter the code sent to your email to verify your identity."
|
||||
"message": "Δεν αναγνωρίζουμε αυτή τη συσκευή. Εισάγετε τον κωδικό που στάλθηκε στο email σας για να επαληθεύσετε την ταυτότητά σας."
|
||||
},
|
||||
"continueLoggingIn": {
|
||||
"message": "Continue logging in"
|
||||
@@ -875,22 +875,22 @@
|
||||
"message": "Σύνδεση στο Bitwarden"
|
||||
},
|
||||
"enterTheCodeSentToYourEmail": {
|
||||
"message": "Enter the code sent to your email"
|
||||
"message": "Εισάγετε τον κωδικό που στάλθηκε στο email σας"
|
||||
},
|
||||
"enterTheCodeFromYourAuthenticatorApp": {
|
||||
"message": "Enter the code from your authenticator app"
|
||||
"message": "Εισαγάγετε τον κωδικό μιας χρήσης από την εφαρμογή αυθεντικοποίησης"
|
||||
},
|
||||
"pressYourYubiKeyToAuthenticate": {
|
||||
"message": "Press your YubiKey to authenticate"
|
||||
"message": "Πιέστε το YubiKey σας για ταυτοποίηση"
|
||||
},
|
||||
"duoTwoFactorRequiredPageSubtitle": {
|
||||
"message": "Duo two-step login is required for your account. Follow the steps below to finish logging in."
|
||||
"message": "Απαιτείται σύνδεση Duo δύο βημάτων για το λογαριασμό σας. Ακολουθήστε τα παρακάτω βήματα για να ολοκληρώσετε τη σύνδεση."
|
||||
},
|
||||
"followTheStepsBelowToFinishLoggingIn": {
|
||||
"message": "Follow the steps below to finish logging in."
|
||||
"message": "Ακολουθήστε τα παρακάτω βήματα για να ολοκληρώσετε τη σύνδεση."
|
||||
},
|
||||
"followTheStepsBelowToFinishLoggingInWithSecurityKey": {
|
||||
"message": "Follow the steps below to finish logging in with your security key."
|
||||
"message": "Ακολουθήστε τα παρακάτω βήματα για να ολοκληρώσετε τη σύνδεση με το κλειδί ασφαλείας σας."
|
||||
},
|
||||
"restartRegistration": {
|
||||
"message": "Επανεκκίνηση εγγραφής"
|
||||
|
||||
@@ -5575,9 +5575,9 @@
|
||||
"description": "'WebAssembly' is a technical term and should not be translated."
|
||||
},
|
||||
"showMore": {
|
||||
"message": "Show more"
|
||||
"message": "Prikaži više"
|
||||
},
|
||||
"showLess": {
|
||||
"message": "Show less"
|
||||
"message": "Pokaži manje"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -605,7 +605,7 @@
|
||||
"message": "Izdzēst vienumu"
|
||||
},
|
||||
"viewItem": {
|
||||
"message": "Skatīt vienumu"
|
||||
"message": "Apskatīt vienumu"
|
||||
},
|
||||
"launch": {
|
||||
"message": "Palaist"
|
||||
@@ -1180,7 +1180,7 @@
|
||||
"message": "Pēc savas paroles nomainīšanas būs nepieciešams pieteikties ar jauno paroli. Spēkā esošajās sesijās citās ierīcēs stundas laikā notiks atteikšanās."
|
||||
},
|
||||
"accountRecoveryUpdateMasterPasswordSubtitle": {
|
||||
"message": "Jānomaina sava galvenā'parole, lai pabeigtu konta atkopi."
|
||||
"message": "Jānomaina sava galvenā parole, lai pabeigtu konta atkopi."
|
||||
},
|
||||
"enableChangedPasswordNotification": {
|
||||
"message": "Vaicāt atjaunināt esošu pieteikšanās vienumu"
|
||||
@@ -1490,7 +1490,7 @@
|
||||
"description": "Select another two-step login method"
|
||||
},
|
||||
"useYourRecoveryCode": {
|
||||
"message": "Izmantot savu atkopes kodu"
|
||||
"message": "Izmanto savu atkopes kodu"
|
||||
},
|
||||
"insertU2f": {
|
||||
"message": "Ievieto savu drošības atslēgu datora USB ligzdā! Ja tai ir poga, pieskaries tai!"
|
||||
@@ -1523,10 +1523,10 @@
|
||||
"message": "Atlasīt divpakāpju pieteikšanās veidu"
|
||||
},
|
||||
"recoveryCodeDesc": {
|
||||
"message": "Zaudēta piekļuve visiem divpakāpju nodrošinātājiem? Izmanto atkopšanas kodus, lai atspējotu visus sava konta divpakāpju nodrošinātājus!"
|
||||
"message": "Zaudēta piekļuve visiem divpakāpju nodrošinātājiem? Izmanto atkopes kodus, lai atspējotu visus sava konta divpakāpju nodrošinātājus!"
|
||||
},
|
||||
"recoveryCodeTitle": {
|
||||
"message": "Atgūšanas kods"
|
||||
"message": "Atkopes kods"
|
||||
},
|
||||
"authenticatorAppTitle": {
|
||||
"message": "Autentificētāja lietotne"
|
||||
@@ -3437,7 +3437,7 @@
|
||||
"message": "Atkārtoti nosūtīt paziņojumu"
|
||||
},
|
||||
"viewAllLogInOptions": {
|
||||
"message": "Skatīt visas pieteikšanās iespējas"
|
||||
"message": "Apskatīt visas pieteikšanās iespējas"
|
||||
},
|
||||
"notificationSentDevice": {
|
||||
"message": "Uz ierīci ir nosūtīts paziņojums."
|
||||
@@ -3728,7 +3728,7 @@
|
||||
"description": "European Union"
|
||||
},
|
||||
"accessDenied": {
|
||||
"message": "Piekļuve liegta. Nav nepieciešamo atļauju, lai skatītu šo lapu."
|
||||
"message": "Piekļuve liegta. Nav nepieciešamo atļauju, lai apskatītu šo lapu."
|
||||
},
|
||||
"general": {
|
||||
"message": "Vispārīgi"
|
||||
@@ -4387,7 +4387,7 @@
|
||||
"description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy"
|
||||
},
|
||||
"startsWithAdvancedOptionWarning": {
|
||||
"message": "\"Sākas ar' ir lietpratējiem paredzēta iespēja ar paaugstinātu piekļuves datu atklāšanas bīstamību.",
|
||||
"message": "“Sākas ar” ir lietpratējiem paredzēta iespēja ar paaugstinātu piekļuves datu atklāšanas bīstamību.",
|
||||
"description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy"
|
||||
},
|
||||
"uriMatchWarningDialogLink": {
|
||||
@@ -4411,7 +4411,7 @@
|
||||
"description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser password management settings"
|
||||
},
|
||||
"confirmContinueToHelpCenterKeyboardShortcutsContent": {
|
||||
"message": "Paplašinājuma īsinājumtaustiņus skatīt un iestatīt var pārlūka iestatījumos.",
|
||||
"message": "Paplašinājuma īsinājumtaustiņus apskatīt un iestatīt var pārlūka iestatījumos.",
|
||||
"description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser keyboard shortcut settings"
|
||||
},
|
||||
"confirmContinueToBrowserPasswordManagementSettingsContent": {
|
||||
@@ -4419,7 +4419,7 @@
|
||||
"description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page"
|
||||
},
|
||||
"confirmContinueToBrowserKeyboardShortcutSettingsContent": {
|
||||
"message": "Paplašinājuma īsinājumtaustiņus skatīt un iestatīt var pārlūka iestatījumos.",
|
||||
"message": "Paplašinājuma īsinājumtaustiņus apskatīt un iestatīt var pārlūka iestatījumos.",
|
||||
"description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page"
|
||||
},
|
||||
"overrideDefaultBrowserAutofillTitle": {
|
||||
@@ -4534,7 +4534,7 @@
|
||||
}
|
||||
},
|
||||
"viewItemTitle": {
|
||||
"message": "Skatīt vienumu - $ITEMNAME$",
|
||||
"message": "Apskatīt vienumu - $ITEMNAME$",
|
||||
"description": "Title for a link that opens a view for an item.",
|
||||
"placeholders": {
|
||||
"itemname": {
|
||||
@@ -4629,7 +4629,7 @@
|
||||
"message": "Kļūda mērķa mapes piešķiršanā."
|
||||
},
|
||||
"viewItemsIn": {
|
||||
"message": "Skatīt $NAME$ vienumus",
|
||||
"message": "Apskatīt $NAME$ vienumus",
|
||||
"description": "Button to view the contents of a folder or collection",
|
||||
"placeholders": {
|
||||
"name": {
|
||||
|
||||
@@ -1153,7 +1153,7 @@
|
||||
"description": "Shown to user after login is updated."
|
||||
},
|
||||
"loginUpdateTaskSuccessAdditional": {
|
||||
"message": "Dziękujemy za dbanie o bezpieczeństwo $ORGANIZATION$. Pozostało $TASK_COUNT$ haseł do zaktualizowania.",
|
||||
"message": "Dziękujemy za zwiększenie bezpieczeństwa organizacji $ORGANIZATION$. Zaktualizuj hasła dla jeszcze $TASK_COUNT$ danych logowania.",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1"
|
||||
@@ -1773,7 +1773,7 @@
|
||||
"message": "Pokaż licznik na ikonie"
|
||||
},
|
||||
"badgeCounterDesc": {
|
||||
"message": "Wskaż, ile masz danych logowania do bieżącej strony internetowej."
|
||||
"message": "Pokazuje liczbę danych logowania dla obecnej strony internetowej."
|
||||
},
|
||||
"cardholderName": {
|
||||
"message": "Właściciel karty"
|
||||
@@ -2482,7 +2482,7 @@
|
||||
"message": "Uprawnienie nie zostało przyznane"
|
||||
},
|
||||
"nativeMessaginPermissionErrorDesc": {
|
||||
"message": "Bez uprawnienia do komunikowania się z aplikacją desktopową Bitwarden nie możemy dostarczyć obsługi danych biometrycznych w rozszerzeniu przeglądarki. Spróbuj ponownie."
|
||||
"message": "Odblokowanie biometrią jest dostępne dopiero po połączeniu rozszerzenia przeglądarki z aplikacją desktopową Bitwarden. Spróbuj ponownie."
|
||||
},
|
||||
"nativeMessaginPermissionSidebarTitle": {
|
||||
"message": "Wystąpił błąd żądania uprawnienia"
|
||||
@@ -2610,7 +2610,7 @@
|
||||
"message": "Sprawdź i zmień 1 zagrożone hasło"
|
||||
},
|
||||
"reviewAndChangeAtRiskPasswordsPlural": {
|
||||
"message": "Przejrzyj i zmień $COUNT$ zagrożonych haseł ",
|
||||
"message": "Sprawdź i zmień zagrożone hasła ($COUNT$)",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
@@ -3380,7 +3380,7 @@
|
||||
"message": "Nie można uzyskać dostępu do elementów w zawieszonych organizacjach. Skontaktuj się z właścicielem organizacji, aby uzyskać pomoc."
|
||||
},
|
||||
"loggingInTo": {
|
||||
"message": "Logowanie na $DOMAIN$",
|
||||
"message": "Serwer: $DOMAIN$",
|
||||
"placeholders": {
|
||||
"domain": {
|
||||
"content": "$1",
|
||||
@@ -4354,7 +4354,7 @@
|
||||
"message": "serwer"
|
||||
},
|
||||
"hostedAt": {
|
||||
"message": "hostowany w"
|
||||
"message": "serwer"
|
||||
},
|
||||
"useDeviceOrHardwareKey": {
|
||||
"message": "Użyj urządzenia lub klucza sprzętowego"
|
||||
@@ -5025,7 +5025,7 @@
|
||||
"message": "Element zostanie przeniesiony do organizacji. Nie będziesz już właścicielem elementu."
|
||||
},
|
||||
"personalItemsTransferWarningPlural": {
|
||||
"message": "$PERSONAL_ITEMS_COUNT$ elementów zostanie trwale przeniesionych do wybranej organizacji. Nie będziesz już posiadać tych elementów.",
|
||||
"message": "Nie będziesz już właścicielem $PERSONAL_ITEMS_COUNT$ elementów przeniesionych do organizacji.",
|
||||
"placeholders": {
|
||||
"personal_items_count": {
|
||||
"content": "$1",
|
||||
@@ -5043,7 +5043,7 @@
|
||||
}
|
||||
},
|
||||
"personalItemsWithOrgTransferWarningPlural": {
|
||||
"message": "$PERSONAL_ITEMS_COUNT$ elementy zostaną trwale przeniesione do $ORG$. Nie będziesz już posiadać tych elementów.",
|
||||
"message": "Nie będziesz już właścicielem $PERSONAL_ITEMS_COUNT$ elementów przeniesionych do organizacji $ORG$.",
|
||||
"placeholders": {
|
||||
"personal_items_count": {
|
||||
"content": "$1",
|
||||
|
||||
@@ -4383,7 +4383,7 @@
|
||||
"description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item."
|
||||
},
|
||||
"regExAdvancedOptionWarning": {
|
||||
"message": "A \"expressão regular\" é uma opção avançada com um risco acrescido de exposição de credenciais.",
|
||||
"message": "A \"Expressão regular\" é uma opção avançada com um risco acrescido de exposição de credenciais.",
|
||||
"description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy"
|
||||
},
|
||||
"startsWithAdvancedOptionWarning": {
|
||||
|
||||
@@ -1836,7 +1836,7 @@
|
||||
"message": "số thẻ"
|
||||
},
|
||||
"ex": {
|
||||
"message": "Ví dụ:"
|
||||
"message": "ví dụ."
|
||||
},
|
||||
"title": {
|
||||
"message": "Tiêu đề"
|
||||
@@ -2458,7 +2458,7 @@
|
||||
"message": "Nhận dạng sinh trắc học không được hỗ trợ"
|
||||
},
|
||||
"biometricsNotSupportedDesc": {
|
||||
"message": "Nhận dạng sinh trắc học trên trình duyệt không được hỗ trợ trên thiết bị này"
|
||||
"message": "Nhận dạng sinh trắc học trên trình duyệt không được hỗ trợ trên thiết bị này."
|
||||
},
|
||||
"biometricsNotUnlockedTitle": {
|
||||
"message": "Người dùng đã khoá hoặc đã đăng xuất"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<button
|
||||
*ngIf="currentAccount$ | async as currentAccount; else defaultButton"
|
||||
type="button"
|
||||
class="tw-rounded-full hover:tw-outline hover:tw-outline-1 hover:tw-outline-offset-1"
|
||||
class="tw-rounded-full hover:tw-outline hover:tw-outline-1 hover:tw-outline-offset-1 hover:tw-outline-primary-600"
|
||||
(click)="currentAccountClicked()"
|
||||
>
|
||||
<span class="tw-sr-only"> {{ "bitwardenAccount" | i18n }} {{ currentAccount.email }}</span>
|
||||
|
||||
@@ -4,11 +4,29 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
import { CollectionView } from "../../content/components/common-types";
|
||||
import { NotificationQueueMessageTypes } from "../../enums/notification-queue-message-type.enum";
|
||||
import { NotificationType, NotificationTypes } from "../../enums/notification-type.enum";
|
||||
import AutofillPageDetails from "../../models/autofill-page-details";
|
||||
|
||||
/**
|
||||
* @todo Remove Standard_ label when implemented as standard NotificationQueueMessage.
|
||||
*/
|
||||
export interface Standard_NotificationQueueMessage<T, D> {
|
||||
// universal notification properties
|
||||
domain: string;
|
||||
tab: chrome.tabs.Tab;
|
||||
launchTimestamp: number;
|
||||
expires: Date;
|
||||
wasVaultLocked: boolean;
|
||||
|
||||
type: T; // NotificationType
|
||||
data: D; // notification-specific data
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Deprecate in favor of Standard_NotificationQueueMessage.
|
||||
*/
|
||||
interface NotificationQueueMessage {
|
||||
type: NotificationQueueMessageTypes;
|
||||
type: NotificationTypes;
|
||||
domain: string;
|
||||
tab: chrome.tabs.Tab;
|
||||
launchTimestamp: number;
|
||||
@@ -16,11 +34,15 @@ interface NotificationQueueMessage {
|
||||
wasVaultLocked: boolean;
|
||||
}
|
||||
|
||||
interface AddChangePasswordQueueMessage extends NotificationQueueMessage {
|
||||
type: "change";
|
||||
type ChangePasswordNotificationData = {
|
||||
cipherId: CipherView["id"];
|
||||
newPassword: string;
|
||||
}
|
||||
};
|
||||
|
||||
type AddChangePasswordNotificationQueueMessage = Standard_NotificationQueueMessage<
|
||||
typeof NotificationType.ChangePassword,
|
||||
ChangePasswordNotificationData
|
||||
>;
|
||||
|
||||
interface AddLoginQueueMessage extends NotificationQueueMessage {
|
||||
type: "add";
|
||||
@@ -41,7 +63,7 @@ interface AtRiskPasswordQueueMessage extends NotificationQueueMessage {
|
||||
|
||||
type NotificationQueueMessageItem =
|
||||
| AddLoginQueueMessage
|
||||
| AddChangePasswordQueueMessage
|
||||
| AddChangePasswordNotificationQueueMessage
|
||||
| AddUnlockVaultQueueMessage
|
||||
| AtRiskPasswordQueueMessage;
|
||||
|
||||
@@ -72,6 +94,11 @@ type UnlockVaultMessageData = {
|
||||
skipNotification?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* @todo Extend generics to this type, see Standard_NotificationQueueMessage
|
||||
* - use new `data` types as generic
|
||||
* - eliminate optional status of properties as needed per Notification Type
|
||||
*/
|
||||
type NotificationBackgroundExtensionMessage = {
|
||||
[key: string]: any;
|
||||
command: string;
|
||||
@@ -126,7 +153,7 @@ type NotificationBackgroundExtensionMessageHandlers = {
|
||||
};
|
||||
|
||||
export {
|
||||
AddChangePasswordQueueMessage,
|
||||
AddChangePasswordNotificationQueueMessage,
|
||||
AddLoginQueueMessage,
|
||||
AddUnlockVaultQueueMessage,
|
||||
NotificationQueueMessageItem,
|
||||
|
||||
@@ -26,14 +26,14 @@ import { FolderService } from "@bitwarden/common/vault/services/folder/folder.se
|
||||
import { TaskService, SecurityTask } from "@bitwarden/common/vault/tasks";
|
||||
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
import { NotificationQueueMessageType } from "../enums/notification-queue-message-type.enum";
|
||||
import { NotificationType } from "../enums/notification-type.enum";
|
||||
import { FormData } from "../services/abstractions/autofill.service";
|
||||
import AutofillService from "../services/autofill.service";
|
||||
import { createAutofillPageDetailsMock, createChromeTabMock } from "../spec/autofill-mocks";
|
||||
import { flushPromises, sendMockExtensionMessage } from "../spec/testing-utils";
|
||||
|
||||
import {
|
||||
AddChangePasswordQueueMessage,
|
||||
AddChangePasswordNotificationQueueMessage,
|
||||
AddLoginQueueMessage,
|
||||
AddUnlockVaultQueueMessage,
|
||||
LockedVaultPendingNotificationsData,
|
||||
@@ -761,7 +761,7 @@ describe("NotificationBackground", () => {
|
||||
notificationBackground["notificationQueue"] = [
|
||||
mock<AddUnlockVaultQueueMessage>({
|
||||
tab,
|
||||
type: NotificationQueueMessageType.UnlockVault,
|
||||
type: NotificationType.UnlockVault,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -783,7 +783,7 @@ describe("NotificationBackground", () => {
|
||||
};
|
||||
notificationBackground["notificationQueue"] = [
|
||||
mock<AddLoginQueueMessage>({
|
||||
type: NotificationQueueMessageType.AddLogin,
|
||||
type: NotificationType.AddLogin,
|
||||
tab,
|
||||
domain: "another.com",
|
||||
}),
|
||||
@@ -803,11 +803,11 @@ describe("NotificationBackground", () => {
|
||||
edit: false,
|
||||
folder: "folder-id",
|
||||
};
|
||||
const queueMessage = mock<AddChangePasswordQueueMessage>({
|
||||
type: NotificationQueueMessageType.ChangePassword,
|
||||
const queueMessage = mock<AddChangePasswordNotificationQueueMessage>({
|
||||
type: NotificationType.ChangePassword,
|
||||
tab,
|
||||
domain: "example.com",
|
||||
newPassword: "newPassword",
|
||||
data: { newPassword: "newPassword" },
|
||||
});
|
||||
notificationBackground["notificationQueue"] = [queueMessage];
|
||||
const cipherView = mock<CipherView>({
|
||||
@@ -825,7 +825,7 @@ describe("NotificationBackground", () => {
|
||||
expect(createWithServerSpy).not.toHaveBeenCalled();
|
||||
expect(updatePasswordSpy).toHaveBeenCalledWith(
|
||||
cipherView,
|
||||
queueMessage.newPassword,
|
||||
queueMessage.data.newPassword,
|
||||
message.edit,
|
||||
sender.tab,
|
||||
"testId",
|
||||
@@ -851,11 +851,11 @@ describe("NotificationBackground", () => {
|
||||
edit: false,
|
||||
folder: "folder-id",
|
||||
};
|
||||
const queueMessage = mock<AddChangePasswordQueueMessage>({
|
||||
type: NotificationQueueMessageType.ChangePassword,
|
||||
const queueMessage = mock<AddChangePasswordNotificationQueueMessage>({
|
||||
type: NotificationType.ChangePassword,
|
||||
tab,
|
||||
domain: "example.com",
|
||||
newPassword: "newPassword",
|
||||
data: { newPassword: "newPassword" },
|
||||
});
|
||||
notificationBackground["notificationQueue"] = [queueMessage];
|
||||
const cipherView = mock<CipherView>({
|
||||
@@ -874,7 +874,7 @@ describe("NotificationBackground", () => {
|
||||
expect(createWithServerSpy).not.toHaveBeenCalled();
|
||||
expect(updatePasswordSpy).toHaveBeenCalledWith(
|
||||
cipherView,
|
||||
queueMessage.newPassword,
|
||||
queueMessage.data.newPassword,
|
||||
message.edit,
|
||||
sender.tab,
|
||||
"testId",
|
||||
@@ -931,11 +931,11 @@ describe("NotificationBackground", () => {
|
||||
edit: false,
|
||||
folder: "folder-id",
|
||||
};
|
||||
const queueMessage = mock<AddChangePasswordQueueMessage>({
|
||||
type: NotificationQueueMessageType.ChangePassword,
|
||||
const queueMessage = mock<AddChangePasswordNotificationQueueMessage>({
|
||||
type: NotificationType.ChangePassword,
|
||||
tab,
|
||||
domain: "example.com",
|
||||
newPassword: "newPassword",
|
||||
data: { newPassword: "newPassword" },
|
||||
});
|
||||
notificationBackground["notificationQueue"] = [queueMessage];
|
||||
const cipherView = mock<CipherView>({
|
||||
@@ -953,7 +953,7 @@ describe("NotificationBackground", () => {
|
||||
expect(createWithServerSpy).not.toHaveBeenCalled();
|
||||
expect(updatePasswordSpy).toHaveBeenCalledWith(
|
||||
cipherView,
|
||||
queueMessage.newPassword,
|
||||
queueMessage.data.newPassword,
|
||||
message.edit,
|
||||
sender.tab,
|
||||
mockCipherId,
|
||||
@@ -983,7 +983,7 @@ describe("NotificationBackground", () => {
|
||||
folder: "folder-id",
|
||||
};
|
||||
const queueMessage = mock<AddLoginQueueMessage>({
|
||||
type: NotificationQueueMessageType.AddLogin,
|
||||
type: NotificationType.AddLogin,
|
||||
tab,
|
||||
domain: "example.com",
|
||||
username: "test",
|
||||
@@ -1018,11 +1018,11 @@ describe("NotificationBackground", () => {
|
||||
edit: true,
|
||||
folder: "folder-id",
|
||||
};
|
||||
const queueMessage = mock<AddChangePasswordQueueMessage>({
|
||||
type: NotificationQueueMessageType.ChangePassword,
|
||||
const queueMessage = mock<AddChangePasswordNotificationQueueMessage>({
|
||||
type: NotificationType.ChangePassword,
|
||||
tab,
|
||||
domain: "example.com",
|
||||
newPassword: "newPassword",
|
||||
data: { newPassword: "newPassword" },
|
||||
});
|
||||
notificationBackground["notificationQueue"] = [queueMessage];
|
||||
const cipherView = mock<CipherView>();
|
||||
@@ -1035,7 +1035,7 @@ describe("NotificationBackground", () => {
|
||||
|
||||
expect(updatePasswordSpy).toHaveBeenCalledWith(
|
||||
cipherView,
|
||||
queueMessage.newPassword,
|
||||
queueMessage.data.newPassword,
|
||||
message.edit,
|
||||
sender.tab,
|
||||
"testId",
|
||||
@@ -1070,7 +1070,7 @@ describe("NotificationBackground", () => {
|
||||
folder: "folder-id",
|
||||
};
|
||||
const queueMessage = mock<AddLoginQueueMessage>({
|
||||
type: NotificationQueueMessageType.AddLogin,
|
||||
type: NotificationType.AddLogin,
|
||||
tab,
|
||||
domain: "example.com",
|
||||
username: "test",
|
||||
@@ -1109,7 +1109,7 @@ describe("NotificationBackground", () => {
|
||||
folder: "folder-id",
|
||||
};
|
||||
const queueMessage = mock<AddLoginQueueMessage>({
|
||||
type: NotificationQueueMessageType.AddLogin,
|
||||
type: NotificationType.AddLogin,
|
||||
tab,
|
||||
domain: "example.com",
|
||||
username: "test",
|
||||
@@ -1162,7 +1162,7 @@ describe("NotificationBackground", () => {
|
||||
folder: "folder-id",
|
||||
};
|
||||
const queueMessage = mock<AddLoginQueueMessage>({
|
||||
type: NotificationQueueMessageType.AddLogin,
|
||||
type: NotificationType.AddLogin,
|
||||
tab,
|
||||
domain: "example.com",
|
||||
username: "test",
|
||||
@@ -1213,11 +1213,11 @@ describe("NotificationBackground", () => {
|
||||
edit: false,
|
||||
folder: "folder-id",
|
||||
};
|
||||
const queueMessage = mock<AddChangePasswordQueueMessage>({
|
||||
type: NotificationQueueMessageType.ChangePassword,
|
||||
const queueMessage = mock<AddChangePasswordNotificationQueueMessage>({
|
||||
type: NotificationType.ChangePassword,
|
||||
tab,
|
||||
domain: "example.com",
|
||||
newPassword: "newPassword",
|
||||
data: { newPassword: "newPassword" },
|
||||
});
|
||||
notificationBackground["notificationQueue"] = [queueMessage];
|
||||
const cipherView = mock<CipherView>({ reprompt: CipherRepromptType.None });
|
||||
@@ -1273,7 +1273,7 @@ describe("NotificationBackground", () => {
|
||||
const sender = mock<chrome.runtime.MessageSender>({ tab });
|
||||
const message: NotificationBackgroundExtensionMessage = { command: "bgNeverSave" };
|
||||
notificationBackground["notificationQueue"] = [
|
||||
mock<AddUnlockVaultQueueMessage>({ type: NotificationQueueMessageType.UnlockVault, tab }),
|
||||
mock<AddUnlockVaultQueueMessage>({ type: NotificationType.UnlockVault, tab }),
|
||||
];
|
||||
|
||||
sendMockExtensionMessage(message, sender);
|
||||
@@ -1289,7 +1289,7 @@ describe("NotificationBackground", () => {
|
||||
const sender = mock<chrome.runtime.MessageSender>({ tab: secondaryTab });
|
||||
notificationBackground["notificationQueue"] = [
|
||||
mock<AddLoginQueueMessage>({
|
||||
type: NotificationQueueMessageType.AddLogin,
|
||||
type: NotificationType.AddLogin,
|
||||
tab,
|
||||
domain: "another.com",
|
||||
}),
|
||||
@@ -1306,12 +1306,12 @@ describe("NotificationBackground", () => {
|
||||
const sender = mock<chrome.runtime.MessageSender>({ tab });
|
||||
const message: NotificationBackgroundExtensionMessage = { command: "bgNeverSave" };
|
||||
const firstNotification = mock<AddLoginQueueMessage>({
|
||||
type: NotificationQueueMessageType.AddLogin,
|
||||
type: NotificationType.AddLogin,
|
||||
tab,
|
||||
domain: "example.com",
|
||||
});
|
||||
const secondNotification = mock<AddLoginQueueMessage>({
|
||||
type: NotificationQueueMessageType.AddLogin,
|
||||
type: NotificationType.AddLogin,
|
||||
tab: createChromeTabMock({ id: 3 }),
|
||||
domain: "another.com",
|
||||
});
|
||||
|
||||
@@ -60,12 +60,12 @@ import {
|
||||
NotificationCipherData,
|
||||
} from "../content/components/cipher/types";
|
||||
import { CollectionView } from "../content/components/common-types";
|
||||
import { NotificationQueueMessageType } from "../enums/notification-queue-message-type.enum";
|
||||
import { NotificationType } from "../enums/notification-type.enum";
|
||||
import { AutofillService } from "../services/abstractions/autofill.service";
|
||||
import { TemporaryNotificationChangeLoginService } from "../services/notification-change-login-password.service";
|
||||
|
||||
import {
|
||||
AddChangePasswordQueueMessage,
|
||||
AddChangePasswordNotificationQueueMessage,
|
||||
AddLoginQueueMessage,
|
||||
AddUnlockVaultQueueMessage,
|
||||
AddLoginMessageData,
|
||||
@@ -208,16 +208,21 @@ export default class NotificationBackground {
|
||||
organizations.find((org) => org.id === orgId)?.productTierType;
|
||||
|
||||
const cipherQueueMessage = this.notificationQueue.find(
|
||||
(message): message is AddChangePasswordQueueMessage | AddLoginQueueMessage =>
|
||||
message.type === NotificationQueueMessageType.ChangePassword ||
|
||||
message.type === NotificationQueueMessageType.AddLogin,
|
||||
(message): message is AddChangePasswordNotificationQueueMessage | AddLoginQueueMessage =>
|
||||
message.type === NotificationType.ChangePassword ||
|
||||
message.type === NotificationType.AddLogin,
|
||||
);
|
||||
|
||||
if (cipherQueueMessage) {
|
||||
const cipherView =
|
||||
cipherQueueMessage.type === NotificationQueueMessageType.ChangePassword
|
||||
? await this.getDecryptedCipherById(cipherQueueMessage.cipherId, activeUserId)
|
||||
: this.convertAddLoginQueueMessageToCipherView(cipherQueueMessage);
|
||||
let cipherView: CipherView;
|
||||
if (cipherQueueMessage.type === NotificationType.ChangePassword) {
|
||||
const {
|
||||
data: { cipherId },
|
||||
} = cipherQueueMessage;
|
||||
cipherView = await this.getDecryptedCipherById(cipherId, activeUserId);
|
||||
} else {
|
||||
cipherView = this.convertAddLoginQueueMessageToCipherView(cipherQueueMessage);
|
||||
}
|
||||
|
||||
const organizationType = getOrganizationType(cipherView.organizationId);
|
||||
return [
|
||||
@@ -424,7 +429,7 @@ export default class NotificationBackground {
|
||||
};
|
||||
|
||||
switch (notificationType) {
|
||||
case NotificationQueueMessageType.AddLogin:
|
||||
case NotificationType.AddLogin:
|
||||
typeData.removeIndividualVault = await this.removeIndividualVault();
|
||||
break;
|
||||
}
|
||||
@@ -501,7 +506,7 @@ export default class NotificationBackground {
|
||||
const queueMessage: NotificationQueueMessageItem = {
|
||||
domain,
|
||||
wasVaultLocked,
|
||||
type: NotificationQueueMessageType.AtRiskPassword,
|
||||
type: NotificationType.AtRiskPassword,
|
||||
passwordChangeUri,
|
||||
organizationName: organization.name,
|
||||
tab: tab,
|
||||
@@ -591,7 +596,7 @@ export default class NotificationBackground {
|
||||
this.removeTabFromNotificationQueue(tab);
|
||||
const launchTimestamp = new Date().getTime();
|
||||
const message: AddLoginQueueMessage = {
|
||||
type: NotificationQueueMessageType.AddLogin,
|
||||
type: NotificationType.AddLogin,
|
||||
username: loginInfo.username,
|
||||
password: loginInfo.password,
|
||||
domain: loginDomain,
|
||||
@@ -716,10 +721,9 @@ export default class NotificationBackground {
|
||||
// remove any old messages for this tab
|
||||
this.removeTabFromNotificationQueue(tab);
|
||||
const launchTimestamp = new Date().getTime();
|
||||
const message: AddChangePasswordQueueMessage = {
|
||||
type: NotificationQueueMessageType.ChangePassword,
|
||||
cipherId: cipherId,
|
||||
newPassword: newPassword,
|
||||
const message: AddChangePasswordNotificationQueueMessage = {
|
||||
type: NotificationType.ChangePassword,
|
||||
data: { cipherId: cipherId, newPassword: newPassword },
|
||||
domain: loginDomain,
|
||||
tab: tab,
|
||||
launchTimestamp,
|
||||
@@ -734,7 +738,7 @@ export default class NotificationBackground {
|
||||
this.removeTabFromNotificationQueue(tab);
|
||||
const launchTimestamp = new Date().getTime();
|
||||
const message: AddUnlockVaultQueueMessage = {
|
||||
type: NotificationQueueMessageType.UnlockVault,
|
||||
type: NotificationType.UnlockVault,
|
||||
domain: loginDomain,
|
||||
tab: tab,
|
||||
launchTimestamp,
|
||||
@@ -804,8 +808,8 @@ export default class NotificationBackground {
|
||||
const queueMessage = this.notificationQueue[i];
|
||||
if (
|
||||
queueMessage.tab.id !== tab.id ||
|
||||
(queueMessage.type !== NotificationQueueMessageType.AddLogin &&
|
||||
queueMessage.type !== NotificationQueueMessageType.ChangePassword)
|
||||
(queueMessage.type !== NotificationType.AddLogin &&
|
||||
queueMessage.type !== NotificationType.ChangePassword)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
@@ -818,17 +822,13 @@ export default class NotificationBackground {
|
||||
this.accountService.activeAccount$.pipe(getOptionalUserId),
|
||||
);
|
||||
|
||||
if (queueMessage.type === NotificationQueueMessageType.ChangePassword) {
|
||||
const cipherView = await this.getDecryptedCipherById(queueMessage.cipherId, activeUserId);
|
||||
if (queueMessage.type === NotificationType.ChangePassword) {
|
||||
const {
|
||||
data: { cipherId, newPassword },
|
||||
} = queueMessage;
|
||||
const cipherView = await this.getDecryptedCipherById(cipherId, activeUserId);
|
||||
|
||||
await this.updatePassword(
|
||||
cipherView,
|
||||
queueMessage.newPassword,
|
||||
edit,
|
||||
tab,
|
||||
activeUserId,
|
||||
skipReprompt,
|
||||
);
|
||||
await this.updatePassword(cipherView, newPassword, edit, tab, activeUserId, skipReprompt);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -993,7 +993,7 @@ export default class NotificationBackground {
|
||||
|
||||
const queueItem = this.notificationQueue.find((item) => item.tab.id === senderTab.id);
|
||||
|
||||
if (queueItem?.type === NotificationQueueMessageType.AddLogin) {
|
||||
if (queueItem?.type === NotificationType.AddLogin) {
|
||||
const cipherView = this.convertAddLoginQueueMessageToCipherView(queueItem);
|
||||
cipherView.organizationId = organizationId;
|
||||
cipherView.folderId = folder;
|
||||
@@ -1075,10 +1075,7 @@ export default class NotificationBackground {
|
||||
private async saveNever(tab: chrome.tabs.Tab) {
|
||||
for (let i = this.notificationQueue.length - 1; i >= 0; i--) {
|
||||
const queueMessage = this.notificationQueue[i];
|
||||
if (
|
||||
queueMessage.tab.id !== tab.id ||
|
||||
queueMessage.type !== NotificationQueueMessageType.AddLogin
|
||||
) {
|
||||
if (queueMessage.tab.id !== tab.id || queueMessage.type !== NotificationType.AddLogin) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ describe("OverlayBackground", () => {
|
||||
fakeStateProvider = new FakeStateProvider(accountService);
|
||||
showFaviconsMock$ = new BehaviorSubject(true);
|
||||
neverDomainsMock$ = new BehaviorSubject({});
|
||||
domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider, configService);
|
||||
domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider);
|
||||
domainSettingsService.showFavicons$ = showFaviconsMock$;
|
||||
domainSettingsService.neverDomains$ = neverDomainsMock$;
|
||||
logService = mock<LogService>();
|
||||
|
||||
@@ -1866,7 +1866,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
frameId: this.focusedFieldData.frameId || 0,
|
||||
},
|
||||
);
|
||||
}, 150);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import {
|
||||
AUTOFILL_CARD_ID,
|
||||
AUTOFILL_ID,
|
||||
@@ -17,7 +18,6 @@ import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/s
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
@@ -67,7 +67,7 @@ const createCipher = (data?: {
|
||||
};
|
||||
|
||||
describe("context-menu", () => {
|
||||
let stateService: MockProxy<StateService>;
|
||||
let tokenService: MockProxy<TokenService>;
|
||||
let autofillSettingsService: MockProxy<AutofillSettingsServiceAbstraction>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let logService: MockProxy<LogService>;
|
||||
@@ -85,7 +85,7 @@ describe("context-menu", () => {
|
||||
let sut: MainContextMenuHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
stateService = mock();
|
||||
tokenService = mock();
|
||||
autofillSettingsService = mock();
|
||||
i18nService = mock();
|
||||
logService = mock();
|
||||
@@ -109,7 +109,7 @@ describe("context-menu", () => {
|
||||
|
||||
i18nService.t.mockImplementation((key) => key);
|
||||
sut = new MainContextMenuHandler(
|
||||
stateService,
|
||||
tokenService,
|
||||
autofillSettingsService,
|
||||
i18nService,
|
||||
logService,
|
||||
@@ -276,7 +276,7 @@ describe("context-menu", () => {
|
||||
it("removes menu items that require code injection", async () => {
|
||||
billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true));
|
||||
autofillSettingsService.enableContextMenu$ = of(true);
|
||||
stateService.getIsAuthenticated.mockResolvedValue(true);
|
||||
tokenService.hasAccessToken$.mockReturnValue(of(true));
|
||||
|
||||
const optionId = "1";
|
||||
await sut.loadOptions("TEST_TITLE", optionId, createCipher());
|
||||
@@ -317,7 +317,7 @@ describe("context-menu", () => {
|
||||
});
|
||||
|
||||
it("Loads context menu items that ask the user to unlock their vault if they are authed", async () => {
|
||||
stateService.getIsAuthenticated.mockResolvedValue(true);
|
||||
tokenService.hasAccessToken$.mockReturnValue(of(true));
|
||||
|
||||
await sut.noAccess();
|
||||
|
||||
@@ -325,7 +325,7 @@ describe("context-menu", () => {
|
||||
});
|
||||
|
||||
it("Loads context menu items that ask the user to login to their vault if they are not authed", async () => {
|
||||
stateService.getIsAuthenticated.mockResolvedValue(false);
|
||||
tokenService.hasAccessToken$.mockReturnValue(of(false));
|
||||
|
||||
await sut.noAccess();
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import {
|
||||
AUTOFILL_CARD_ID,
|
||||
AUTOFILL_ID,
|
||||
@@ -23,7 +24,6 @@ import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/s
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
@@ -152,7 +152,7 @@ export class MainContextMenuHandler {
|
||||
];
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private tokenService: TokenService,
|
||||
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
||||
private i18nService: I18nService,
|
||||
private logService: LogService,
|
||||
@@ -343,7 +343,11 @@ export class MainContextMenuHandler {
|
||||
|
||||
async noAccess() {
|
||||
if (await this.init()) {
|
||||
const authed = await this.stateService.getIsAuthenticated();
|
||||
const userId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const authed =
|
||||
userId != null && (await firstValueFrom(this.tokenService.hasAccessToken$(userId)));
|
||||
this.loadOptions(
|
||||
this.i18nService.t(authed ? "unlockVaultMenu" : "loginToVaultMenu"),
|
||||
NOOP_COMMAND_SUFFIX,
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
const NotificationQueueMessageType = {
|
||||
AddLogin: "add",
|
||||
ChangePassword: "change",
|
||||
UnlockVault: "unlock",
|
||||
AtRiskPassword: "at-risk-password",
|
||||
} as const;
|
||||
|
||||
type NotificationQueueMessageTypes =
|
||||
(typeof NotificationQueueMessageType)[keyof typeof NotificationQueueMessageType];
|
||||
|
||||
export { NotificationQueueMessageType, NotificationQueueMessageTypes };
|
||||
10
apps/browser/src/autofill/enums/notification-type.enum.ts
Normal file
10
apps/browser/src/autofill/enums/notification-type.enum.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
const NotificationType = {
|
||||
AddLogin: "add",
|
||||
ChangePassword: "change",
|
||||
UnlockVault: "unlock",
|
||||
AtRiskPassword: "at-risk-password",
|
||||
} as const;
|
||||
|
||||
type NotificationTypes = (typeof NotificationType)[keyof typeof NotificationType];
|
||||
|
||||
export { NotificationType, NotificationTypes };
|
||||
@@ -14,6 +14,10 @@ const NotificationTypes = {
|
||||
AtRiskPassword: "at-risk-password",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* @todo Deprecate in favor of apps/browser/src/autofill/enums/notification-type.enum.ts
|
||||
* - Determine fix or workaround for restricted imports of that file.
|
||||
*/
|
||||
type NotificationType = (typeof NotificationTypes)[keyof typeof NotificationTypes];
|
||||
|
||||
type NotificationTaskInfo = {
|
||||
@@ -21,6 +25,9 @@ type NotificationTaskInfo = {
|
||||
remainingTasksCount: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* @todo Use generics to make this type specific to notification types, see Standard_NotificationQueueMessage.
|
||||
*/
|
||||
type NotificationBarIframeInitData = {
|
||||
ciphers?: NotificationCipherData[];
|
||||
folders?: FolderView[];
|
||||
|
||||
@@ -29,7 +29,7 @@ describe("AutofillInlineMenuContentService", () => {
|
||||
autofillInit = new AutofillInit(
|
||||
domQueryService,
|
||||
domElementVisibilityService,
|
||||
null,
|
||||
undefined,
|
||||
autofillInlineMenuContentService,
|
||||
);
|
||||
autofillInit.init();
|
||||
@@ -319,6 +319,8 @@ describe("AutofillInlineMenuContentService", () => {
|
||||
|
||||
describe("handleContainerElementMutationObserverUpdate", () => {
|
||||
let mockMutationRecord: MockProxy<MutationRecord>;
|
||||
let mockBodyMutationRecord: MockProxy<MutationRecord>;
|
||||
let mockHTMLMutationRecord: MockProxy<MutationRecord>;
|
||||
let buttonElement: HTMLElement;
|
||||
let listElement: HTMLElement;
|
||||
let isInlineMenuListVisibleSpy: jest.SpyInstance;
|
||||
@@ -329,6 +331,16 @@ describe("AutofillInlineMenuContentService", () => {
|
||||
<div class="overlay-list"></div>
|
||||
`;
|
||||
mockMutationRecord = mock<MutationRecord>({ target: globalThis.document.body } as any);
|
||||
mockHTMLMutationRecord = mock<MutationRecord>({
|
||||
target: globalThis.document.body.parentElement,
|
||||
attributeName: "style",
|
||||
type: "attributes",
|
||||
} as any);
|
||||
mockBodyMutationRecord = mock<MutationRecord>({
|
||||
target: globalThis.document.body,
|
||||
attributeName: "style",
|
||||
type: "attributes",
|
||||
} as any);
|
||||
buttonElement = document.querySelector(".overlay-button") as HTMLElement;
|
||||
listElement = document.querySelector(".overlay-list") as HTMLElement;
|
||||
autofillInlineMenuContentService["buttonElement"] = buttonElement;
|
||||
@@ -343,6 +355,7 @@ describe("AutofillInlineMenuContentService", () => {
|
||||
"isTriggeringExcessiveMutationObserverIterations",
|
||||
)
|
||||
.mockReturnValue(false);
|
||||
jest.spyOn(autofillInlineMenuContentService as any, "closeInlineMenu");
|
||||
});
|
||||
|
||||
it("skips handling the mutation if the overlay elements are not present in the DOM", async () => {
|
||||
@@ -373,6 +386,33 @@ describe("AutofillInlineMenuContentService", () => {
|
||||
expect(globalThis.document.body.insertBefore).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("closes the inline menu if the page body is not sufficiently opaque", async () => {
|
||||
document.querySelector("html").style.opacity = "0.9";
|
||||
document.body.style.opacity = "0";
|
||||
autofillInlineMenuContentService["handlePageMutations"]([mockBodyMutationRecord]);
|
||||
|
||||
expect(autofillInlineMenuContentService["pageIsOpaque"]).toBe(false);
|
||||
expect(autofillInlineMenuContentService["closeInlineMenu"]).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("closes the inline menu if the page html is not sufficiently opaque", async () => {
|
||||
document.querySelector("html").style.opacity = "0.3";
|
||||
document.body.style.opacity = "0.7";
|
||||
autofillInlineMenuContentService["handlePageMutations"]([mockHTMLMutationRecord]);
|
||||
|
||||
expect(autofillInlineMenuContentService["pageIsOpaque"]).toBe(false);
|
||||
expect(autofillInlineMenuContentService["closeInlineMenu"]).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not close the inline menu if the page html and body is sufficiently opaque", async () => {
|
||||
document.querySelector("html").style.opacity = "0.9";
|
||||
document.body.style.opacity = "1";
|
||||
autofillInlineMenuContentService["handlePageMutations"]([mockBodyMutationRecord]);
|
||||
|
||||
expect(autofillInlineMenuContentService["pageIsOpaque"]).toBe(true);
|
||||
expect(autofillInlineMenuContentService["closeInlineMenu"]).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips re-arranging the DOM elements if the last child of the body is non-existent", async () => {
|
||||
document.body.innerHTML = "";
|
||||
|
||||
|
||||
@@ -29,8 +29,11 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
|
||||
private isFirefoxBrowser =
|
||||
globalThis.navigator.userAgent.indexOf(" Firefox/") !== -1 ||
|
||||
globalThis.navigator.userAgent.indexOf(" Gecko/") !== -1;
|
||||
private buttonElement: HTMLElement;
|
||||
private listElement: HTMLElement;
|
||||
private buttonElement?: HTMLElement;
|
||||
private listElement?: HTMLElement;
|
||||
private htmlMutationObserver: MutationObserver;
|
||||
private bodyMutationObserver: MutationObserver;
|
||||
private pageIsOpaque = true;
|
||||
private inlineMenuElementsMutationObserver: MutationObserver;
|
||||
private containerElementMutationObserver: MutationObserver;
|
||||
private mutationObserverIterations = 0;
|
||||
@@ -49,6 +52,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
|
||||
};
|
||||
|
||||
constructor() {
|
||||
this.checkPageOpacity();
|
||||
this.setupMutationObserver();
|
||||
}
|
||||
|
||||
@@ -281,6 +285,9 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
|
||||
* that the inline menu elements are always present at the bottom of the menu container.
|
||||
*/
|
||||
private setupMutationObserver = () => {
|
||||
this.htmlMutationObserver = new MutationObserver(this.handlePageMutations);
|
||||
this.bodyMutationObserver = new MutationObserver(this.handlePageMutations);
|
||||
|
||||
this.inlineMenuElementsMutationObserver = new MutationObserver(
|
||||
this.handleInlineMenuElementMutationObserverUpdate,
|
||||
);
|
||||
@@ -295,6 +302,9 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
|
||||
* elements are not modified by the website.
|
||||
*/
|
||||
private observeCustomElements() {
|
||||
this.htmlMutationObserver?.observe(document.querySelector("html"), { attributes: true });
|
||||
this.bodyMutationObserver?.observe(document.body, { attributes: true });
|
||||
|
||||
if (this.buttonElement) {
|
||||
this.inlineMenuElementsMutationObserver?.observe(this.buttonElement, {
|
||||
attributes: true,
|
||||
@@ -395,11 +405,56 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte
|
||||
});
|
||||
};
|
||||
|
||||
private checkPageOpacity = () => {
|
||||
this.pageIsOpaque = this.getPageIsOpaque();
|
||||
|
||||
if (!this.pageIsOpaque) {
|
||||
this.closeInlineMenu();
|
||||
}
|
||||
};
|
||||
|
||||
private handlePageMutations = (mutations: MutationRecord[]) => {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type === "attributes") {
|
||||
this.checkPageOpacity();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks the opacity of the page body and body parent, since the inline menu experience
|
||||
* will inherit the opacity, despite being otherwise encapsulated from styling changes
|
||||
* of parents below the body. Assumes the target element will be a direct child of the page
|
||||
* `body` (enforced elsewhere).
|
||||
*/
|
||||
private getPageIsOpaque() {
|
||||
// These are computed style values, so we don't need to worry about non-float values
|
||||
// for `opacity`, here
|
||||
const htmlOpacity = globalThis.window.getComputedStyle(
|
||||
globalThis.document.querySelector("html"),
|
||||
).opacity;
|
||||
const bodyOpacity = globalThis.window.getComputedStyle(
|
||||
globalThis.document.querySelector("body"),
|
||||
).opacity;
|
||||
|
||||
// Any value above this is considered "opaque" for our purposes
|
||||
const opacityThreshold = 0.6;
|
||||
|
||||
return parseFloat(htmlOpacity) > opacityThreshold && parseFloat(bodyOpacity) > opacityThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the mutation of the element that contains the inline menu. Will trigger when an
|
||||
* idle moment in the execution of the main thread is detected.
|
||||
*/
|
||||
private processContainerElementMutation = async (containerElement: HTMLElement) => {
|
||||
// If the computed opacity of the body and parent is not sufficiently opaque, tear
|
||||
// down and prevent building the inline menu experience.
|
||||
this.checkPageOpacity();
|
||||
if (!this.pageIsOpaque) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastChild = containerElement.lastElementChild;
|
||||
const secondToLastChild = lastChild?.previousElementSibling;
|
||||
const lastChildIsInlineMenuList = lastChild === this.listElement;
|
||||
|
||||
@@ -213,7 +213,7 @@
|
||||
</bit-card>
|
||||
</form>
|
||||
</bit-section>
|
||||
<bit-section [disableMargin]="!blockBrowserInjectionsByDomainEnabled">
|
||||
<bit-section>
|
||||
<form [formGroup]="additionalOptionsForm">
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">{{ "additionalOptions" | i18n }}</h2>
|
||||
@@ -276,7 +276,7 @@
|
||||
</bit-card>
|
||||
</form>
|
||||
</bit-section>
|
||||
<bit-section *ngIf="blockBrowserInjectionsByDomainEnabled" disableMargin>
|
||||
<bit-section disableMargin>
|
||||
<bit-item>
|
||||
<a bit-item-content routerLink="/blocked-domains">{{ "blockedDomains" | i18n }}</a>
|
||||
<i slot="end" class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||
|
||||
@@ -44,7 +44,6 @@ import {
|
||||
DisablePasswordManagerUri,
|
||||
InlineMenuVisibilitySetting,
|
||||
} from "@bitwarden/common/autofill/types";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import {
|
||||
UriMatchStrategy,
|
||||
UriMatchStrategySetting,
|
||||
@@ -110,7 +109,6 @@ export class AutofillComponent implements OnInit {
|
||||
protected defaultBrowserAutofillDisabled: boolean = false;
|
||||
protected inlineMenuVisibility: InlineMenuVisibilitySetting =
|
||||
AutofillOverlayVisibility.OnFieldFocus;
|
||||
protected blockBrowserInjectionsByDomainEnabled: boolean = false;
|
||||
protected browserClientVendor: BrowserClientVendor = BrowserClientVendors.Unknown;
|
||||
protected disablePasswordManagerURI: DisablePasswordManagerUri =
|
||||
DisablePasswordManagerUris.Unknown;
|
||||
@@ -222,10 +220,6 @@ export class AutofillComponent implements OnInit {
|
||||
this.autofillSettingsService.inlineMenuVisibility$,
|
||||
);
|
||||
|
||||
this.blockBrowserInjectionsByDomainEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.BlockBrowserInjectionsByDomain,
|
||||
);
|
||||
|
||||
this.showInlineMenuIdentities = await firstValueFrom(
|
||||
this.autofillSettingsService.showInlineMenuIdentities$,
|
||||
);
|
||||
|
||||
@@ -138,7 +138,7 @@ describe("AutofillService", () => {
|
||||
userNotificationsSettings,
|
||||
messageListener,
|
||||
);
|
||||
domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider, configService);
|
||||
domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider);
|
||||
domainSettingsService.equivalentDomains$ = of(mockEquivalentDomains);
|
||||
jest.spyOn(BrowserApi, "tabSendMessage");
|
||||
});
|
||||
|
||||
@@ -2459,22 +2459,23 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
break;
|
||||
}
|
||||
|
||||
const includesUsernameFieldName =
|
||||
this.findMatchingFieldIndex(f, AutoFillConstants.UsernameFieldNames) > -1;
|
||||
|
||||
if (
|
||||
!f.disabled &&
|
||||
(canBeReadOnly || !f.readonly) &&
|
||||
(withoutForm || f.form === passwordField.form) &&
|
||||
(withoutForm || f.form === passwordField.form || includesUsernameFieldName) &&
|
||||
(canBeHidden || f.viewable) &&
|
||||
(f.type === "text" || f.type === "email" || f.type === "tel")
|
||||
) {
|
||||
usernameField = f;
|
||||
|
||||
if (this.findMatchingFieldIndex(f, AutoFillConstants.UsernameFieldNames) > -1) {
|
||||
// We found an exact match. No need to keep looking.
|
||||
// We found an exact match. No need to keep looking.
|
||||
if (includesUsernameFieldName) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return usernameField;
|
||||
}
|
||||
|
||||
|
||||
@@ -1757,6 +1757,54 @@ describe("CollectAutofillContentService", () => {
|
||||
|
||||
expect(parsedText).toEqual("Hello! This is a test string.");
|
||||
});
|
||||
|
||||
it("preserves extended Latin letters like Š and ć", () => {
|
||||
const text = "Šifra ćevapčići korisnika";
|
||||
const result = collectAutofillContentService["trimAndRemoveNonPrintableText"](text);
|
||||
expect(result).toEqual("Šifra ćevapčići korisnika");
|
||||
});
|
||||
|
||||
it("removes zero-width and control characters", () => {
|
||||
const text = "Hello\u200B\u200C\u200D\u2060World\x00\x1F!";
|
||||
const result = collectAutofillContentService["trimAndRemoveNonPrintableText"](text);
|
||||
expect(result).toEqual("Hello World !");
|
||||
});
|
||||
|
||||
it("removes leading and trailing whitespace", () => {
|
||||
const text = " padded text with spaces ";
|
||||
const result = collectAutofillContentService["trimAndRemoveNonPrintableText"](text);
|
||||
expect(result).toEqual("padded text with spaces");
|
||||
});
|
||||
|
||||
it("replaces multiple whitespaces (tabs, newlines, spaces) with one space", () => {
|
||||
const text = "one\t\ntwo \n three\t\tfour";
|
||||
const result = collectAutofillContentService["trimAndRemoveNonPrintableText"](text);
|
||||
expect(result).toEqual("one two three four");
|
||||
});
|
||||
|
||||
it("preserves emoji and symbols", () => {
|
||||
const text = "Text with emoji 🐍🚀 and ©®✓ symbols";
|
||||
const result = collectAutofillContentService["trimAndRemoveNonPrintableText"](text);
|
||||
expect(result).toEqual("Text with emoji 🐍🚀 and ©®✓ symbols");
|
||||
});
|
||||
|
||||
it("handles RTL and LTR marks", () => {
|
||||
const text = "abc\u200F\u202Edеf";
|
||||
const result = collectAutofillContentService["trimAndRemoveNonPrintableText"](text);
|
||||
expect(result).toEqual("abc dеf");
|
||||
});
|
||||
|
||||
it("handles mathematical unicode letters", () => {
|
||||
const text = "Unicode math: 𝒜𝒷𝒸𝒹";
|
||||
const result = collectAutofillContentService["trimAndRemoveNonPrintableText"](text);
|
||||
expect(result).toEqual("Unicode math: 𝒜𝒷𝒸𝒹");
|
||||
});
|
||||
|
||||
it("removes only invisible non-printables, keeps Japanese", () => {
|
||||
const text = "これは\u200Bテストです";
|
||||
const result = collectAutofillContentService["trimAndRemoveNonPrintableText"](text);
|
||||
expect(result).toEqual("これは テストです");
|
||||
});
|
||||
});
|
||||
|
||||
describe("recursivelyGetTextFromPreviousSiblings", () => {
|
||||
|
||||
@@ -713,7 +713,7 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
||||
*/
|
||||
private trimAndRemoveNonPrintableText(textContent: string): string {
|
||||
return (textContent || "")
|
||||
.replace(/[^\x20-\x7E]+|\s+/g, " ") // Strip out non-primitive characters and replace multiple spaces with a single space
|
||||
.replace(/\p{C}+|\s+/gu, " ") // Strip out non-printable characters and replace multiple spaces with a single space
|
||||
.trim(); // Trim leading and trailing whitespace
|
||||
}
|
||||
|
||||
|
||||
@@ -68,8 +68,11 @@ import { isUrlInList } from "@bitwarden/common/autofill/utils";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
|
||||
import {
|
||||
DefaultKeyGenerationService,
|
||||
KeyGenerationService,
|
||||
} from "@bitwarden/common/key-management/crypto";
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/encrypt.service.implementation";
|
||||
@@ -98,7 +101,6 @@ import { Fido2ClientService as Fido2ClientServiceAbstraction } from "@bitwarden/
|
||||
import { Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-user-interface.service.abstraction";
|
||||
import { FileUploadService as FileUploadServiceAbstraction } from "@bitwarden/common/platform/abstractions/file-upload/file-upload.service";
|
||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||
import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
@@ -109,14 +111,11 @@ import {
|
||||
ObservableStorageService,
|
||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/platform/abstractions/system.service";
|
||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||
import { IpcService } from "@bitwarden/common/platform/ipc";
|
||||
import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
// eslint-disable-next-line no-restricted-imports -- Used for dependency creation
|
||||
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
|
||||
import { Lazy } from "@bitwarden/common/platform/misc/lazy";
|
||||
import { Account } from "@bitwarden/common/platform/models/domain/account";
|
||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { NotificationsService } from "@bitwarden/common/platform/notifications";
|
||||
// eslint-disable-next-line no-restricted-imports -- Needed for service creation
|
||||
@@ -136,17 +135,16 @@ import { Fido2ActiveRequestManager } from "@bitwarden/common/platform/services/f
|
||||
import { Fido2AuthenticatorService } from "@bitwarden/common/platform/services/fido2/fido2-authenticator.service";
|
||||
import { Fido2ClientService } from "@bitwarden/common/platform/services/fido2/fido2-client.service";
|
||||
import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
|
||||
import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service";
|
||||
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||
import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory";
|
||||
import { DefaultSdkService } from "@bitwarden/common/platform/services/sdk/default-sdk.service";
|
||||
import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory";
|
||||
import { StateService } from "@bitwarden/common/platform/services/state.service";
|
||||
import { SystemService } from "@bitwarden/common/platform/services/system.service";
|
||||
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
|
||||
import {
|
||||
ActiveUserStateProvider,
|
||||
DefaultStateService,
|
||||
DerivedStateProvider,
|
||||
GlobalStateProvider,
|
||||
SingleUserStateProvider,
|
||||
@@ -312,7 +310,7 @@ export default class MainBackground {
|
||||
i18nService: I18nServiceAbstraction;
|
||||
platformUtilsService: PlatformUtilsServiceAbstraction;
|
||||
logService: LogServiceAbstraction;
|
||||
keyGenerationService: KeyGenerationServiceAbstraction;
|
||||
keyGenerationService: KeyGenerationService;
|
||||
keyService: KeyServiceAbstraction;
|
||||
cryptoFunctionService: CryptoFunctionServiceAbstraction;
|
||||
masterPasswordService: InternalMasterPasswordServiceAbstraction;
|
||||
@@ -386,6 +384,7 @@ export default class MainBackground {
|
||||
activeUserStateProvider: ActiveUserStateProvider;
|
||||
derivedStateProvider: DerivedStateProvider;
|
||||
stateProvider: StateProvider;
|
||||
migrationRunner: MigrationRunner;
|
||||
taskSchedulerService: BrowserTaskSchedulerService;
|
||||
fido2Background: Fido2BackgroundAbstraction;
|
||||
individualVaultExportService: IndividualVaultExportServiceAbstraction;
|
||||
@@ -473,7 +472,7 @@ export default class MainBackground {
|
||||
const isDev = process.env.ENV === "development";
|
||||
this.logService = new ConsoleLogService(isDev);
|
||||
this.cryptoFunctionService = new WebCryptoFunctionService(self);
|
||||
this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService);
|
||||
this.keyGenerationService = new DefaultKeyGenerationService(this.cryptoFunctionService);
|
||||
this.storageService = new BrowserLocalStorageService(this.logService);
|
||||
|
||||
this.intraprocessMessagingSubject = new Subject<Message<Record<string, unknown>>>();
|
||||
@@ -591,8 +590,9 @@ export default class MainBackground {
|
||||
this.globalStateProvider,
|
||||
this.singleUserStateProvider,
|
||||
);
|
||||
const activeUserAccessor = new DefaultActiveUserAccessor(this.accountService);
|
||||
this.activeUserStateProvider = new DefaultActiveUserStateProvider(
|
||||
new DefaultActiveUserAccessor(this.accountService),
|
||||
activeUserAccessor,
|
||||
this.singleUserStateProvider,
|
||||
);
|
||||
this.derivedStateProvider = new InlineDerivedStateProvider();
|
||||
@@ -638,23 +638,17 @@ export default class MainBackground {
|
||||
this.taskSchedulerService,
|
||||
);
|
||||
|
||||
const migrationRunner = new MigrationRunner(
|
||||
this.migrationRunner = new MigrationRunner(
|
||||
this.storageService,
|
||||
this.logService,
|
||||
new MigrationBuilderService(),
|
||||
ClientType.Browser,
|
||||
);
|
||||
|
||||
this.stateService = new StateService(
|
||||
this.stateService = new DefaultStateService(
|
||||
this.storageService,
|
||||
this.secureStorageService,
|
||||
this.memoryStorageService,
|
||||
this.logService,
|
||||
new StateFactory(GlobalState, Account),
|
||||
this.accountService,
|
||||
this.environmentService,
|
||||
this.tokenService,
|
||||
migrationRunner,
|
||||
activeUserAccessor,
|
||||
);
|
||||
|
||||
this.masterPasswordService = new MasterPasswordService(
|
||||
@@ -871,10 +865,7 @@ export default class MainBackground {
|
||||
|
||||
this.userVerificationApiService = new UserVerificationApiService(this.apiService);
|
||||
|
||||
this.domainSettingsService = new DefaultDomainSettingsService(
|
||||
this.stateProvider,
|
||||
this.configService,
|
||||
);
|
||||
this.domainSettingsService = new DefaultDomainSettingsService(this.stateProvider);
|
||||
|
||||
this.themeStateService = new DefaultThemeStateService(this.globalStateProvider);
|
||||
|
||||
@@ -889,7 +880,6 @@ export default class MainBackground {
|
||||
this.apiService,
|
||||
this.i18nService,
|
||||
this.searchService,
|
||||
this.stateService,
|
||||
this.autofillSettingsService,
|
||||
this.encryptService,
|
||||
this.cipherFileUploadService,
|
||||
@@ -948,6 +938,7 @@ export default class MainBackground {
|
||||
this.messagingService,
|
||||
this.searchService,
|
||||
this.stateService,
|
||||
this.tokenService,
|
||||
this.authService,
|
||||
this.vaultTimeoutSettingsService,
|
||||
this.stateEventRunnerService,
|
||||
@@ -991,7 +982,6 @@ export default class MainBackground {
|
||||
this.sendService,
|
||||
this.logService,
|
||||
this.keyConnectorService,
|
||||
this.stateService,
|
||||
this.providerService,
|
||||
this.folderApiService,
|
||||
this.organizationService,
|
||||
@@ -1322,7 +1312,7 @@ export default class MainBackground {
|
||||
);
|
||||
|
||||
this.mainContextMenuHandler = new MainContextMenuHandler(
|
||||
this.stateService,
|
||||
this.tokenService,
|
||||
this.autofillSettingsService,
|
||||
this.i18nService,
|
||||
this.logService,
|
||||
@@ -1389,7 +1379,7 @@ export default class MainBackground {
|
||||
|
||||
await this.sdkLoadService.loadAndInit();
|
||||
// Only the "true" background should run migrations
|
||||
await this.stateService.init({ runMigrations: true });
|
||||
await this.migrationRunner.run();
|
||||
|
||||
// This is here instead of in in the InitService b/c we don't plan for
|
||||
// side effects to run in the Browser InitService.
|
||||
@@ -1446,10 +1436,7 @@ export default class MainBackground {
|
||||
this.notificationsService.startListening();
|
||||
|
||||
this.taskService.listenForTaskNotifications();
|
||||
|
||||
if (await this.configService.getFeatureFlag(FeatureFlag.EndUserNotifications)) {
|
||||
this.endUserNotificationService.listenForEndUserNotifications();
|
||||
}
|
||||
this.endUserNotificationService.listenForEndUserNotifications();
|
||||
resolve();
|
||||
}, 500);
|
||||
});
|
||||
@@ -1612,6 +1599,7 @@ export default class MainBackground {
|
||||
const needStorageReseed = await this.needsStorageReseed(userBeingLoggedOut);
|
||||
|
||||
await this.stateService.clean({ userId: userBeingLoggedOut });
|
||||
await this.tokenService.clearAccessToken(userBeingLoggedOut);
|
||||
await this.accountService.clean(userBeingLoggedOut);
|
||||
|
||||
await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { map, Observable } from "rxjs";
|
||||
import { concat, defer, filter, map, merge, Observable, shareReplay, switchMap } from "rxjs";
|
||||
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
@@ -17,19 +17,48 @@ export interface RawBadgeState {
|
||||
|
||||
export interface BadgeBrowserApi {
|
||||
activeTab$: Observable<chrome.tabs.TabActiveInfo | undefined>;
|
||||
// activeTabs$: Observable<chrome.tabs.Tab[]>;
|
||||
|
||||
setState(state: RawBadgeState, tabId?: number): Promise<void>;
|
||||
getTabs(): Promise<number[]>;
|
||||
getActiveTabs(): Promise<chrome.tabs.Tab[]>;
|
||||
}
|
||||
|
||||
export class DefaultBadgeBrowserApi implements BadgeBrowserApi {
|
||||
private badgeAction = BrowserApi.getBrowserAction();
|
||||
private sidebarAction = BrowserApi.getSidebarAction(self);
|
||||
|
||||
activeTab$ = fromChromeEvent(chrome.tabs.onActivated).pipe(
|
||||
map(([tabActiveInfo]) => tabActiveInfo),
|
||||
private onTabActivated$ = fromChromeEvent(chrome.tabs.onActivated).pipe(
|
||||
switchMap(async ([activeInfo]) => activeInfo),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
activeTab$ = concat(
|
||||
defer(async () => {
|
||||
const currentTab = await BrowserApi.getTabFromCurrentWindow();
|
||||
if (currentTab == null || currentTab.id === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { tabId: currentTab.id, windowId: currentTab.windowId };
|
||||
}),
|
||||
merge(
|
||||
this.onTabActivated$,
|
||||
fromChromeEvent(chrome.tabs.onUpdated).pipe(
|
||||
filter(
|
||||
([_, changeInfo]) =>
|
||||
// Only emit if the url was updated
|
||||
changeInfo.url != undefined,
|
||||
),
|
||||
map(([tabId, _changeInfo, tab]) => ({ tabId, windowId: tab.windowId })),
|
||||
),
|
||||
),
|
||||
).pipe(shareReplay({ bufferSize: 1, refCount: true }));
|
||||
|
||||
getActiveTabs(): Promise<chrome.tabs.Tab[]> {
|
||||
return BrowserApi.getActiveTabs();
|
||||
}
|
||||
|
||||
constructor(private platformUtilsService: PlatformUtilsService) {}
|
||||
|
||||
async setState(state: RawBadgeState, tabId?: number): Promise<void> {
|
||||
|
||||
@@ -37,8 +37,9 @@ describe("BadgeService", () => {
|
||||
|
||||
describe("given a single tab is open", () => {
|
||||
beforeEach(() => {
|
||||
badgeApi.tabs = [1];
|
||||
badgeApi.setActiveTab(tabId);
|
||||
badgeApi.tabs = [tabId];
|
||||
badgeApi.setActiveTabs([tabId]);
|
||||
badgeApi.setLastActivatedTab(tabId);
|
||||
badgeServiceSubscription = badgeService.startListening();
|
||||
});
|
||||
|
||||
@@ -187,17 +188,18 @@ describe("BadgeService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("given multiple tabs are open", () => {
|
||||
describe("given multiple tabs are open, only one active", () => {
|
||||
const tabId = 1;
|
||||
const tabIds = [1, 2, 3];
|
||||
|
||||
beforeEach(() => {
|
||||
badgeApi.tabs = tabIds;
|
||||
badgeApi.setActiveTab(tabId);
|
||||
badgeApi.setActiveTabs([tabId]);
|
||||
badgeApi.setLastActivatedTab(tabId);
|
||||
badgeServiceSubscription = badgeService.startListening();
|
||||
});
|
||||
|
||||
it("sets state for each tab when no other state has been set", async () => {
|
||||
it("sets general state for active tab when no other state has been set", async () => {
|
||||
const state: BadgeState = {
|
||||
text: "text",
|
||||
backgroundColor: "color",
|
||||
@@ -213,6 +215,67 @@ describe("BadgeService", () => {
|
||||
3: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("only updates the active tab when setting state", async () => {
|
||||
const state: BadgeState = {
|
||||
text: "text",
|
||||
backgroundColor: "color",
|
||||
icon: BadgeIcon.Locked,
|
||||
};
|
||||
badgeApi.setState.mockReset();
|
||||
|
||||
await badgeService.setState("state-1", BadgeStatePriority.Default, state, tabId);
|
||||
await badgeService.setState("state-2", BadgeStatePriority.Default, state, 2);
|
||||
await badgeService.setState("state-2", BadgeStatePriority.Default, state, 2);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(badgeApi.setState).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given multiple tabs are open and multiple are active", () => {
|
||||
const activeTabIds = [1, 2];
|
||||
const tabIds = [1, 2, 3];
|
||||
|
||||
beforeEach(() => {
|
||||
badgeApi.tabs = tabIds;
|
||||
badgeApi.setActiveTabs(activeTabIds);
|
||||
badgeApi.setLastActivatedTab(1);
|
||||
badgeServiceSubscription = badgeService.startListening();
|
||||
});
|
||||
|
||||
it("sets general state for active tabs when no other state has been set", async () => {
|
||||
const state: BadgeState = {
|
||||
text: "text",
|
||||
backgroundColor: "color",
|
||||
icon: BadgeIcon.Locked,
|
||||
};
|
||||
|
||||
await badgeService.setState("state-name", BadgeStatePriority.Default, state);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(badgeApi.specificStates).toEqual({
|
||||
1: state,
|
||||
2: state,
|
||||
3: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("only updates the active tabs when setting general state", async () => {
|
||||
const state: BadgeState = {
|
||||
text: "text",
|
||||
backgroundColor: "color",
|
||||
icon: BadgeIcon.Locked,
|
||||
};
|
||||
badgeApi.setState.mockReset();
|
||||
|
||||
await badgeService.setState("state-1", BadgeStatePriority.Default, state, 1);
|
||||
await badgeService.setState("state-2", BadgeStatePriority.Default, state, 2);
|
||||
await badgeService.setState("state-3", BadgeStatePriority.Default, state, 3);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(badgeApi.setState).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -222,7 +285,8 @@ describe("BadgeService", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
badgeApi.tabs = [tabId];
|
||||
badgeApi.setActiveTab(tabId);
|
||||
badgeApi.setActiveTabs([tabId]);
|
||||
badgeApi.setLastActivatedTab(tabId);
|
||||
badgeServiceSubscription = badgeService.startListening();
|
||||
});
|
||||
|
||||
@@ -491,13 +555,14 @@ describe("BadgeService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("given multiple tabs are open", () => {
|
||||
describe("given multiple tabs are open, only one active", () => {
|
||||
const tabId = 1;
|
||||
const tabIds = [1, 2, 3];
|
||||
|
||||
beforeEach(() => {
|
||||
badgeApi.tabs = tabIds;
|
||||
badgeApi.setActiveTab(tabId);
|
||||
badgeApi.setActiveTabs([tabId]);
|
||||
badgeApi.setLastActivatedTab(tabId);
|
||||
badgeServiceSubscription = badgeService.startListening();
|
||||
});
|
||||
|
||||
@@ -528,5 +593,62 @@ describe("BadgeService", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("given multiple tabs are open and multiple are active", () => {
|
||||
const tabId = 1;
|
||||
const activeTabIds = [1, 2];
|
||||
const tabIds = [1, 2, 3];
|
||||
|
||||
beforeEach(() => {
|
||||
badgeApi.tabs = tabIds;
|
||||
badgeApi.setActiveTabs(activeTabIds);
|
||||
badgeApi.setLastActivatedTab(tabId);
|
||||
badgeServiceSubscription = badgeService.startListening();
|
||||
});
|
||||
|
||||
it("sets general state for all active tabs when no other state has been set", async () => {
|
||||
const generalState: BadgeState = {
|
||||
text: "general-text",
|
||||
backgroundColor: "general-color",
|
||||
icon: BadgeIcon.Unlocked,
|
||||
};
|
||||
|
||||
await badgeService.setState("general-state", BadgeStatePriority.Default, generalState);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(badgeApi.specificStates).toEqual({
|
||||
[tabIds[0]]: generalState,
|
||||
[tabIds[1]]: generalState,
|
||||
[tabIds[2]]: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("sets tab-specific state for provided tab", async () => {
|
||||
const generalState: BadgeState = {
|
||||
text: "general-text",
|
||||
backgroundColor: "general-color",
|
||||
icon: BadgeIcon.Unlocked,
|
||||
};
|
||||
const specificState: BadgeState = {
|
||||
text: "tab-text",
|
||||
icon: BadgeIcon.Locked,
|
||||
};
|
||||
|
||||
await badgeService.setState("general-state", BadgeStatePriority.Default, generalState);
|
||||
await badgeService.setState(
|
||||
"tab-state",
|
||||
BadgeStatePriority.Default,
|
||||
specificState,
|
||||
tabIds[0],
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(badgeApi.specificStates).toEqual({
|
||||
[tabIds[0]]: { ...specificState, backgroundColor: "general-color" },
|
||||
[tabIds[1]]: generalState,
|
||||
[tabIds[2]]: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
import {
|
||||
combineLatest,
|
||||
concatMap,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
map,
|
||||
pairwise,
|
||||
startWith,
|
||||
Subscription,
|
||||
} from "rxjs";
|
||||
import { concatMap, filter, Subscription, withLatestFrom } from "rxjs";
|
||||
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import {
|
||||
@@ -17,7 +8,6 @@ import {
|
||||
StateProvider,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
|
||||
import { difference } from "./array-utils";
|
||||
import { BadgeBrowserApi, RawBadgeState } from "./badge-browser-api";
|
||||
import { DefaultBadgeState } from "./consts";
|
||||
import { BadgeStatePriority } from "./priority";
|
||||
@@ -34,14 +24,14 @@ const BADGE_STATES = new KeyDefinition(BADGE_MEMORY, "badgeStates", {
|
||||
});
|
||||
|
||||
export class BadgeService {
|
||||
private states: GlobalState<Record<string, StateSetting>>;
|
||||
private serviceState: GlobalState<Record<string, StateSetting>>;
|
||||
|
||||
constructor(
|
||||
private stateProvider: StateProvider,
|
||||
private badgeApi: BadgeBrowserApi,
|
||||
private logService: LogService,
|
||||
) {
|
||||
this.states = this.stateProvider.getGlobal(BADGE_STATES);
|
||||
this.serviceState = this.stateProvider.getGlobal(BADGE_STATES);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,44 +39,20 @@ export class BadgeService {
|
||||
* Without this the service will not be able to update the badge state.
|
||||
*/
|
||||
startListening(): Subscription {
|
||||
return combineLatest({
|
||||
states: this.states.state$.pipe(
|
||||
startWith({}),
|
||||
distinctUntilChanged(),
|
||||
map((states) => new Set(states ? Object.values(states) : [])),
|
||||
pairwise(),
|
||||
map(([previous, current]) => {
|
||||
const [removed, added] = difference(previous, current);
|
||||
return { all: current, removed, added };
|
||||
}),
|
||||
filter(({ removed, added }) => removed.size > 0 || added.size > 0),
|
||||
),
|
||||
activeTab: this.badgeApi.activeTab$.pipe(startWith(undefined)),
|
||||
})
|
||||
// React to tab changes
|
||||
return this.badgeApi.activeTab$
|
||||
.pipe(
|
||||
concatMap(async ({ states, activeTab }) => {
|
||||
const changed = [...states.removed, ...states.added];
|
||||
|
||||
// If the active tab wasn't changed, we don't need to update the badge.
|
||||
if (!changed.some((s) => s.tabId === activeTab?.tabId || s.tabId === undefined)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const state = this.calculateState(states.all, activeTab?.tabId);
|
||||
await this.badgeApi.setState(state, activeTab?.tabId);
|
||||
} catch (error) {
|
||||
// This usually happens when the user opens a popout because of how the browser treats it
|
||||
// as a tab in the same window but then won't let you set the badge state for it.
|
||||
this.logService.warning("Failed to set badge state", error);
|
||||
}
|
||||
withLatestFrom(this.serviceState.state$),
|
||||
filter(([activeTab]) => activeTab != undefined),
|
||||
concatMap(async ([activeTab, serviceState]) => {
|
||||
await this.updateBadge(serviceState, activeTab!.tabId);
|
||||
}),
|
||||
)
|
||||
.subscribe({
|
||||
error: (err: unknown) => {
|
||||
error: (error: unknown) => {
|
||||
this.logService.error(
|
||||
"Fatal error in badge service observable, badge will fail to update",
|
||||
err,
|
||||
error,
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -108,7 +74,12 @@ export class BadgeService {
|
||||
* @param tabId Limit this badge state to a specific tab. If this is not set, the state will be applied to all tabs.
|
||||
*/
|
||||
async setState(name: string, priority: BadgeStatePriority, state: BadgeState, tabId?: number) {
|
||||
await this.states.update((s) => ({ ...s, [name]: { priority, state, tabId } }));
|
||||
const newServiceState = await this.serviceState.update((s) => ({
|
||||
...s,
|
||||
[name]: { priority, state, tabId },
|
||||
}));
|
||||
|
||||
await this.updateBadge(newServiceState, tabId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,11 +91,21 @@ export class BadgeService {
|
||||
* @param name The name of the state to clear.
|
||||
*/
|
||||
async clearState(name: string) {
|
||||
await this.states.update((s) => {
|
||||
let clearedState: StateSetting | undefined;
|
||||
|
||||
const newServiceState = await this.serviceState.update((s) => {
|
||||
clearedState = s?.[name];
|
||||
|
||||
const newStates = { ...s };
|
||||
delete newStates[name];
|
||||
return newStates;
|
||||
});
|
||||
|
||||
if (clearedState === undefined) {
|
||||
return;
|
||||
}
|
||||
// const activeTabs = await firstValueFrom(this.badgeApi.activeTabs$);
|
||||
await this.updateBadge(newServiceState, clearedState.tabId);
|
||||
}
|
||||
|
||||
private calculateState(states: Set<StateSetting>, tabId?: number): RawBadgeState {
|
||||
@@ -159,6 +140,52 @@ export class BadgeService {
|
||||
...mergedState,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Common function deduplicating the logic for updating the badge with the current state.
|
||||
* This will only update the badge if the active tab is the same as the tabId of the latest change.
|
||||
* If the active tab is not set, it will not update the badge.
|
||||
*
|
||||
* @param activeTab The currently active tab.
|
||||
* @param serviceState The current state of the badge service. If this is null or undefined, an empty set will be assumed.
|
||||
* @param tabId Tab id for which the the latest state change applied to. Set this to activeTab.tabId to force an update.
|
||||
*/
|
||||
private async updateBadge(
|
||||
// activeTabs: chrome.tabs.Tab[],
|
||||
serviceState: Record<string, StateSetting> | null | undefined,
|
||||
tabId: number | undefined,
|
||||
) {
|
||||
const activeTabs = await this.badgeApi.getActiveTabs();
|
||||
if (tabId !== undefined && !activeTabs.some((tab) => tab.id === tabId)) {
|
||||
return; // No need to update the badge if the state is not for the active tab.
|
||||
}
|
||||
|
||||
const tabIdsToUpdate = tabId ? [tabId] : activeTabs.map((tab) => tab.id);
|
||||
|
||||
for (const tabId of tabIdsToUpdate) {
|
||||
if (tabId === undefined) {
|
||||
continue; // Skip if tab id is undefined.
|
||||
}
|
||||
|
||||
const newBadgeState = this.calculateState(new Set(Object.values(serviceState ?? {})), tabId);
|
||||
try {
|
||||
await this.badgeApi.setState(newBadgeState, tabId);
|
||||
} catch (error) {
|
||||
this.logService.error("Failed to set badge state", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (tabId === undefined) {
|
||||
// If no tabId was provided we should also update the general badge state
|
||||
const newBadgeState = this.calculateState(new Set(Object.values(serviceState ?? {})));
|
||||
|
||||
try {
|
||||
await this.badgeApi.setState(newBadgeState, tabId);
|
||||
} catch (error) {
|
||||
this.logService.error("Failed to set general badge state", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,15 +9,33 @@ export class MockBadgeBrowserApi implements BadgeBrowserApi {
|
||||
specificStates: Record<number, RawBadgeState> = {};
|
||||
generalState?: RawBadgeState;
|
||||
tabs: number[] = [];
|
||||
activeTabs: number[] = [];
|
||||
|
||||
setActiveTab(tabId: number) {
|
||||
getActiveTabs(): Promise<chrome.tabs.Tab[]> {
|
||||
return Promise.resolve(
|
||||
this.activeTabs.map(
|
||||
(tabId) =>
|
||||
({
|
||||
id: tabId,
|
||||
windowId: 1,
|
||||
active: true,
|
||||
}) as chrome.tabs.Tab,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
setActiveTabs(tabs: number[]) {
|
||||
this.activeTabs = tabs;
|
||||
}
|
||||
|
||||
setLastActivatedTab(tabId: number) {
|
||||
this._activeTab$.next({
|
||||
tabId,
|
||||
windowId: 1,
|
||||
});
|
||||
}
|
||||
|
||||
setState(state: RawBadgeState, tabId?: number): Promise<void> {
|
||||
setState = jest.fn().mockImplementation((state: RawBadgeState, tabId?: number): Promise<void> => {
|
||||
if (tabId !== undefined) {
|
||||
this.specificStates[tabId] = state;
|
||||
} else {
|
||||
@@ -25,7 +43,7 @@ export class MockBadgeBrowserApi implements BadgeBrowserApi {
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
getTabs(): Promise<number[]> {
|
||||
return Promise.resolve(this.tabs);
|
||||
|
||||
@@ -32,6 +32,15 @@ export class BrowserApi {
|
||||
return BrowserApi.manifestVersion === expectedVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all open browser windows, including their tabs.
|
||||
*
|
||||
* @returns A promise that resolves to an array of browser windows.
|
||||
*/
|
||||
static async getWindows(): Promise<chrome.windows.Window[]> {
|
||||
return new Promise((resolve) => chrome.windows.getAll({ populate: true }, resolve));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current window or the window with the given id.
|
||||
*
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Directive, Optional } from "@angular/core";
|
||||
import { Directive, inject, model } from "@angular/core";
|
||||
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { BitActionDirective, ButtonLikeAbstraction } from "@bitwarden/components";
|
||||
import { BitActionDirective, FunctionReturningAwaitable } from "@bitwarden/components";
|
||||
|
||||
import { PopupRouterCacheService } from "../view-cache/popup-router-cache.service";
|
||||
|
||||
@@ -11,15 +9,10 @@ import { PopupRouterCacheService } from "../view-cache/popup-router-cache.servic
|
||||
selector: "[popupBackAction]",
|
||||
})
|
||||
export class PopupBackBrowserDirective extends BitActionDirective {
|
||||
constructor(
|
||||
buttonComponent: ButtonLikeAbstraction,
|
||||
private router: PopupRouterCacheService,
|
||||
@Optional() validationService?: ValidationService,
|
||||
@Optional() logService?: LogService,
|
||||
) {
|
||||
super(buttonComponent, validationService, logService);
|
||||
|
||||
// override `bitAction` input; the parent handles the rest
|
||||
this.handler.set(() => this.router.back());
|
||||
}
|
||||
private routerCacheService = inject(PopupRouterCacheService);
|
||||
// Override the required input to make it optional since we set it automatically
|
||||
override readonly handler = model<FunctionReturningAwaitable>(
|
||||
() => this.routerCacheService.back(),
|
||||
{ alias: "popupBackAction" },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ class MockPopoutButtonComponent {}
|
||||
@Component({
|
||||
selector: "mock-current-account",
|
||||
template: `
|
||||
<button class="tw-bg-transparent tw-border-none tw-p-0 tw-me-1" type="button">
|
||||
<button class="tw-bg-transparent tw-border-none tw-p-0 tw-me-1 tw-align-middle" type="button">
|
||||
<bit-avatar text="Ash Ketchum" size="small"></bit-avatar>
|
||||
</button>
|
||||
`,
|
||||
@@ -343,7 +343,7 @@ export default {
|
||||
generator: "Generator",
|
||||
send: "Send",
|
||||
settings: "Settings",
|
||||
labelWithNotification: (label: string) => `${label}: New Notification`,
|
||||
labelWithNotification: (label: string | undefined) => `${label}: New Notification`,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import {
|
||||
DomainSettingsService,
|
||||
DefaultDomainSettingsService,
|
||||
} from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
@@ -54,14 +53,11 @@ describe("ScriptInjectorService", () => {
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
|
||||
const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService);
|
||||
let configService: MockProxy<ConfigService>;
|
||||
let domainSettingsService: DomainSettingsService;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(BrowserApi, "getTab").mockImplementation(async () => tabMock);
|
||||
configService = mock<ConfigService>();
|
||||
configService.getFeatureFlag$.mockImplementation(() => of(false));
|
||||
domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider, configService);
|
||||
domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider);
|
||||
domainSettingsService.equivalentDomains$ = of(mockEquivalentDomains);
|
||||
domainSettingsService.blockedInteractionsUris$ = of({});
|
||||
scriptInjectorService = new BrowserScriptInjectorService(
|
||||
|
||||
@@ -4,8 +4,8 @@ import { Subject } from "rxjs";
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SyncOptions } from "@bitwarden/common/platform/sync/sync.service";
|
||||
@@ -22,7 +22,7 @@ import { FullSyncFinishedMessage } from "./sync-service.listener";
|
||||
|
||||
describe("ForegroundSyncService", () => {
|
||||
const userId = Utils.newGuid() as UserId;
|
||||
const stateService = mock<StateService>();
|
||||
const tokenService = mock<TokenService>();
|
||||
const folderService = mock<InternalFolderService>();
|
||||
const folderApiService = mock<FolderApiServiceAbstraction>();
|
||||
const messageSender = mock<MessageSender>();
|
||||
@@ -38,7 +38,7 @@ describe("ForegroundSyncService", () => {
|
||||
const stateProvider = new FakeStateProvider(accountService);
|
||||
|
||||
const sut = new ForegroundSyncService(
|
||||
stateService,
|
||||
tokenService,
|
||||
folderService,
|
||||
folderApiService,
|
||||
messageSender,
|
||||
|
||||
@@ -4,8 +4,8 @@ import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import {
|
||||
CommandDefinition,
|
||||
MessageListener,
|
||||
@@ -31,7 +31,7 @@ export const DO_FULL_SYNC = new CommandDefinition<FullSyncMessage>("doFullSync")
|
||||
|
||||
export class ForegroundSyncService extends CoreSyncService {
|
||||
constructor(
|
||||
stateService: StateService,
|
||||
tokenService: TokenService,
|
||||
folderService: InternalFolderService,
|
||||
folderApiService: FolderApiServiceAbstraction,
|
||||
messageSender: MessageSender,
|
||||
@@ -47,7 +47,7 @@ export class ForegroundSyncService extends CoreSyncService {
|
||||
stateProvider: StateProvider,
|
||||
) {
|
||||
super(
|
||||
stateService,
|
||||
tokenService,
|
||||
folderService,
|
||||
folderApiService,
|
||||
messageSender,
|
||||
|
||||
@@ -2,11 +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 {
|
||||
EnvironmentSelectorComponent,
|
||||
EnvironmentSelectorRouteData,
|
||||
ExtensionDefaultOverlayPosition,
|
||||
} from "@bitwarden/angular/auth/components/environment-selector.component";
|
||||
import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/environment-selector/environment-selector.component";
|
||||
import {
|
||||
activeAuthGuard,
|
||||
authGuard,
|
||||
@@ -400,9 +396,6 @@ const routes: Routes = [
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
data: {
|
||||
overlayPosition: ExtensionDefaultOverlayPosition,
|
||||
} satisfies EnvironmentSelectorRouteData,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -425,9 +418,6 @@ const routes: Routes = [
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
data: {
|
||||
overlayPosition: ExtensionDefaultOverlayPosition,
|
||||
} satisfies EnvironmentSelectorRouteData,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -474,9 +464,6 @@ const routes: Routes = [
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
data: {
|
||||
overlayPosition: ExtensionDefaultOverlayPosition,
|
||||
} satisfies EnvironmentSelectorRouteData,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -28,13 +28,13 @@ import { DocumentLangSetter } from "@bitwarden/angular/platform/i18n";
|
||||
import { LogoutReason, UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { AnimationControlService } from "@bitwarden/common/platform/abstractions/animation-control.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { MessageListener } from "@bitwarden/common/platform/messaging";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
@@ -102,7 +102,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
private authService: AuthService,
|
||||
private i18nService: I18nService,
|
||||
private router: Router,
|
||||
private stateService: StateService,
|
||||
private readonly tokenService: TokenService,
|
||||
private vaultBrowserStateService: VaultBrowserStateService,
|
||||
private cipherService: CipherService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
@@ -321,7 +321,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private async clearComponentStates() {
|
||||
if (!(await this.stateService.getIsAuthenticated())) {
|
||||
if (!(await firstValueFrom(this.tokenService.hasAccessToken$(this.activeUserId)))) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import { FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||
import { BrowserModule } from "@angular/platform-browser";
|
||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
|
||||
import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-count.pipe";
|
||||
import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe";
|
||||
@@ -96,7 +95,6 @@ import "../platform/popup/locales";
|
||||
TabsV2Component,
|
||||
UserVerificationComponent,
|
||||
RemovePasswordComponent,
|
||||
EnvironmentSelectorComponent,
|
||||
],
|
||||
exports: [],
|
||||
providers: [CurrencyPipe, DatePipe],
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
@import "../../../../../libs/components/src/tw-theme.css";
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer components {
|
||||
/** Safari Support */
|
||||
html.browser_safari .tw-styled-scrollbar::-webkit-scrollbar {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../platform/browser/browser-popup-utils";
|
||||
@@ -27,13 +28,14 @@ export class InitService {
|
||||
private themingService: AbstractThemingService,
|
||||
private sdkLoadService: SdkLoadService,
|
||||
private viewCacheService: PopupViewCacheService,
|
||||
private readonly migrationRunner: MigrationRunner,
|
||||
@Inject(DOCUMENT) private document: Document,
|
||||
) {}
|
||||
|
||||
init() {
|
||||
return async () => {
|
||||
await this.sdkLoadService.loadAndInit();
|
||||
await this.stateService.init({ runMigrations: false }); // Browser background is responsible for migrations
|
||||
await this.migrationRunner.waitForCompletion(); // Browser background is responsible for migrations
|
||||
await this.i18nService.init();
|
||||
this.twoFactorService.init();
|
||||
await this.viewCacheService.init();
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import {
|
||||
AutofillSettingsService,
|
||||
@@ -62,6 +63,7 @@ import {
|
||||
} from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { WebCryptoFunctionService } from "@bitwarden/common/key-management/crypto/services/web-crypto-function.service";
|
||||
@@ -79,7 +81,6 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@@ -333,7 +334,7 @@ const safeProviders: SafeProvider[] = [
|
||||
provide: SyncService,
|
||||
useClass: ForegroundSyncService,
|
||||
deps: [
|
||||
StateService,
|
||||
TokenService,
|
||||
InternalFolderService,
|
||||
FolderApiServiceAbstraction,
|
||||
MessageSender,
|
||||
@@ -352,7 +353,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: DomainSettingsService,
|
||||
useClass: DefaultDomainSettingsService,
|
||||
deps: [StateProvider, ConfigService],
|
||||
deps: [StateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AbstractStorageService,
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { combineLatest, map, Observable, startWith, switchMap } from "rxjs";
|
||||
import { map, Observable, startWith, switchMap } from "rxjs";
|
||||
|
||||
import { NudgesService } from "@bitwarden/angular/vault";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { Icons } from "@bitwarden/components";
|
||||
|
||||
import { NavButton } from "../platform/popup/layout/popup-tab-navigation.component";
|
||||
@@ -19,12 +17,9 @@ export class TabsV2Component {
|
||||
private hasActiveBadges$ = this.accountService.activeAccount$
|
||||
.pipe(getUserId)
|
||||
.pipe(switchMap((userId) => this.nudgesService.hasActiveBadges$(userId)));
|
||||
protected navButtons$: Observable<NavButton[]> = combineLatest([
|
||||
this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge),
|
||||
this.hasActiveBadges$,
|
||||
]).pipe(
|
||||
startWith([false, false]),
|
||||
map(([onboardingFeatureEnabled, hasBadges]) => {
|
||||
protected navButtons$: Observable<NavButton[]> = this.hasActiveBadges$.pipe(
|
||||
startWith(false),
|
||||
map((hasBadges) => {
|
||||
return [
|
||||
{
|
||||
label: "vault",
|
||||
@@ -49,7 +44,7 @@ export class TabsV2Component {
|
||||
page: "/tabs/settings",
|
||||
icon: Icons.SettingsInactive,
|
||||
iconActive: Icons.SettingsActive,
|
||||
showBerry: onboardingFeatureEnabled && hasBadges,
|
||||
showBerry: hasBadges,
|
||||
},
|
||||
];
|
||||
}),
|
||||
@@ -57,6 +52,5 @@ export class TabsV2Component {
|
||||
constructor(
|
||||
private nudgesService: NudgesService,
|
||||
private accountService: AccountService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -23,12 +23,6 @@
|
||||
<i slot="end" class="bwi bwi-external-link" aria-hidden="true"></i>
|
||||
</button>
|
||||
</bit-item>
|
||||
<bit-item *ngIf="!(isNudgeFeatureEnabled$ | async)">
|
||||
<a bit-item-content routerLink="/more-from-bitwarden">
|
||||
{{ "moreFromBitwarden" | i18n }}
|
||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
</bit-item>
|
||||
<bit-item>
|
||||
<button type="button" bit-item-content (click)="rate()">
|
||||
{{ "rateExtension" | i18n }}
|
||||
|
||||
@@ -5,8 +5,6 @@ import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { DeviceType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { DialogService, ItemModule } from "@bitwarden/components";
|
||||
@@ -48,17 +46,12 @@ export class AboutPageV2Component {
|
||||
private dialogService: DialogService,
|
||||
private environmentService: EnvironmentService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
about() {
|
||||
this.dialogService.open(AboutDialogComponent);
|
||||
}
|
||||
|
||||
protected isNudgeFeatureEnabled$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.PM8851_BrowserOnboardingNudge,
|
||||
);
|
||||
|
||||
async launchHelp() {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "continueToHelpCenter" },
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
</bit-item>
|
||||
<bit-item *ngIf="isNudgeFeatureEnabled$ | async">
|
||||
<bit-item>
|
||||
<a bit-item-content routerLink="/download-bitwarden">
|
||||
<i slot="start" class="bwi bwi-mobile" aria-hidden="true"></i>
|
||||
<div class="tw-flex tw-items-center tw-justify-center">
|
||||
@@ -92,7 +92,7 @@
|
||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
</bit-item>
|
||||
<bit-item *ngIf="isNudgeFeatureEnabled$ | async">
|
||||
<bit-item>
|
||||
<a bit-item-content routerLink="/more-from-bitwarden">
|
||||
<i slot="start" class="bwi bwi-filter" aria-hidden="true"></i>
|
||||
{{ "moreFromBitwarden" | i18n }}
|
||||
|
||||
@@ -14,8 +14,6 @@ import {
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { BadgeComponent, ItemModule } from "@bitwarden/components";
|
||||
|
||||
@@ -75,15 +73,10 @@ export class SettingsV2Component implements OnInit {
|
||||
),
|
||||
);
|
||||
|
||||
protected isNudgeFeatureEnabled$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.PM8851_BrowserOnboardingNudge,
|
||||
);
|
||||
|
||||
constructor(
|
||||
private readonly nudgesService: NudgesService,
|
||||
private readonly accountService: AccountService,
|
||||
private readonly autofillBrowserSettingsService: AutofillBrowserSettingsService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
|
||||
@@ -12,7 +12,6 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
|
||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
@@ -74,7 +73,6 @@ describe("AtRiskPasswordsComponent", () => {
|
||||
const mockAtRiskPasswordPageService = mock<AtRiskPasswordPageService>();
|
||||
const mockChangeLoginPasswordService = mock<ChangeLoginPasswordService>();
|
||||
const mockDialogService = mock<DialogService>();
|
||||
const mockConfigService = mock<ConfigService>();
|
||||
|
||||
beforeEach(async () => {
|
||||
mockTasks$ = new BehaviorSubject<SecurityTask[]>([
|
||||
@@ -113,7 +111,6 @@ describe("AtRiskPasswordsComponent", () => {
|
||||
setInlineMenuVisibility.mockClear();
|
||||
mockToastService.showToast.mockClear();
|
||||
mockDialogService.open.mockClear();
|
||||
mockConfigService.getFeatureFlag.mockClear();
|
||||
mockAtRiskPasswordPageService.isCalloutDismissed.mockReturnValue(calloutDismissed$);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
@@ -155,7 +152,6 @@ describe("AtRiskPasswordsComponent", () => {
|
||||
},
|
||||
},
|
||||
{ provide: ToastService, useValue: mockToastService },
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
],
|
||||
})
|
||||
.overrideModule(JslibModule, {
|
||||
|
||||
@@ -23,8 +23,6 @@ import {
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
|
||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
@@ -96,7 +94,6 @@ export class AtRiskPasswordsComponent implements OnInit {
|
||||
private platformUtilsService = inject(PlatformUtilsService);
|
||||
private dialogService = inject(DialogService);
|
||||
private endUserNotificationService = inject(EndUserNotificationService);
|
||||
private configService = inject(ConfigService);
|
||||
private destroyRef = inject(DestroyRef);
|
||||
|
||||
/**
|
||||
@@ -201,9 +198,7 @@ export class AtRiskPasswordsComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
if (await this.configService.getFeatureFlag(FeatureFlag.EndUserNotifications)) {
|
||||
this.markTaskNotificationsAsRead();
|
||||
}
|
||||
this.markTaskNotificationsAsRead();
|
||||
}
|
||||
|
||||
private markTaskNotificationsAsRead() {
|
||||
|
||||
@@ -78,14 +78,14 @@ describe("ViewV2Component", () => {
|
||||
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
|
||||
|
||||
const mockCipherService = {
|
||||
get: jest.fn().mockResolvedValue({ decrypt: jest.fn().mockResolvedValue(mockCipher) }),
|
||||
cipherViews$: jest.fn().mockImplementation((userId) => of([mockCipher])),
|
||||
getKeyForCipherKeyDecryption: jest.fn().mockResolvedValue({}),
|
||||
deleteWithServer: jest.fn().mockResolvedValue(undefined),
|
||||
softDeleteWithServer: jest.fn().mockResolvedValue(undefined),
|
||||
decrypt: jest.fn().mockResolvedValue(mockCipher),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
mockCipherService.cipherViews$.mockClear();
|
||||
mockCipherService.deleteWithServer.mockClear();
|
||||
mockCipherService.softDeleteWithServer.mockClear();
|
||||
mockNavigate.mockClear();
|
||||
@@ -162,7 +162,7 @@ describe("ViewV2Component", () => {
|
||||
|
||||
flush(); // Resolve all promises
|
||||
|
||||
expect(mockCipherService.get).toHaveBeenCalledWith("122-333-444", mockUserId);
|
||||
expect(mockCipherService.cipherViews$).toHaveBeenCalledWith(mockUserId);
|
||||
expect(component.cipher).toEqual(mockCipher);
|
||||
}));
|
||||
|
||||
@@ -210,7 +210,7 @@ describe("ViewV2Component", () => {
|
||||
}));
|
||||
|
||||
it('invokes `doAutofill` when action="AUTOFILL_ID"', fakeAsync(() => {
|
||||
params$.next({ action: AUTOFILL_ID });
|
||||
params$.next({ action: AUTOFILL_ID, cipherId: mockCipher.id });
|
||||
|
||||
flush(); // Resolve all promises
|
||||
|
||||
@@ -218,7 +218,7 @@ describe("ViewV2Component", () => {
|
||||
}));
|
||||
|
||||
it('invokes `copy` when action="copy-username"', fakeAsync(() => {
|
||||
params$.next({ action: COPY_USERNAME_ID });
|
||||
params$.next({ action: COPY_USERNAME_ID, cipherId: mockCipher.id });
|
||||
|
||||
flush(); // Resolve all promises
|
||||
|
||||
@@ -226,7 +226,7 @@ describe("ViewV2Component", () => {
|
||||
}));
|
||||
|
||||
it('invokes `copy` when action="copy-password"', fakeAsync(() => {
|
||||
params$.next({ action: COPY_PASSWORD_ID });
|
||||
params$.next({ action: COPY_PASSWORD_ID, cipherId: mockCipher.id });
|
||||
|
||||
flush(); // Resolve all promises
|
||||
|
||||
@@ -234,7 +234,7 @@ describe("ViewV2Component", () => {
|
||||
}));
|
||||
|
||||
it('invokes `copy` when action="copy-totp"', fakeAsync(() => {
|
||||
params$.next({ action: COPY_VERIFICATION_CODE_ID });
|
||||
params$.next({ action: COPY_VERIFICATION_CODE_ID, cipherId: mockCipher.id });
|
||||
|
||||
flush(); // Resolve all promises
|
||||
|
||||
@@ -243,11 +243,13 @@ describe("ViewV2Component", () => {
|
||||
|
||||
it("does not set the cipher until reprompt is complete", fakeAsync(() => {
|
||||
let promptPromise: (val?: unknown) => void;
|
||||
mockCipherService.decrypt.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
...mockCipher,
|
||||
reprompt: CipherRepromptType.Password,
|
||||
}),
|
||||
mockCipherService.cipherViews$.mockImplementationOnce((userId) =>
|
||||
of([
|
||||
{
|
||||
...mockCipher,
|
||||
reprompt: CipherRepromptType.Password,
|
||||
},
|
||||
]),
|
||||
);
|
||||
doAutofill.mockImplementationOnce(() => {
|
||||
return new Promise((resolve) => {
|
||||
@@ -256,7 +258,7 @@ describe("ViewV2Component", () => {
|
||||
});
|
||||
});
|
||||
|
||||
params$.next({ action: AUTOFILL_ID });
|
||||
params$.next({ action: AUTOFILL_ID, cipherId: mockCipher.id });
|
||||
|
||||
flush(); // Flush all pending actions
|
||||
|
||||
@@ -271,11 +273,13 @@ describe("ViewV2Component", () => {
|
||||
|
||||
it("does not set the cipher at all if doAutofill fails and reprompt is active", fakeAsync(() => {
|
||||
let promptPromise: (val?: unknown) => void;
|
||||
mockCipherService.decrypt.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
...mockCipher,
|
||||
reprompt: CipherRepromptType.Password,
|
||||
}),
|
||||
mockCipherService.cipherViews$.mockImplementationOnce((userId) =>
|
||||
of([
|
||||
{
|
||||
...mockCipher,
|
||||
reprompt: CipherRepromptType.Password,
|
||||
},
|
||||
]),
|
||||
);
|
||||
doAutofill.mockImplementationOnce(() => {
|
||||
return new Promise((resolve) => {
|
||||
@@ -284,7 +288,7 @@ describe("ViewV2Component", () => {
|
||||
});
|
||||
});
|
||||
|
||||
params$.next({ action: AUTOFILL_ID });
|
||||
params$.next({ action: AUTOFILL_ID, cipherId: mockCipher.id });
|
||||
|
||||
flush(); // Flush all pending actions
|
||||
|
||||
@@ -301,11 +305,13 @@ describe("ViewV2Component", () => {
|
||||
"does not set cipher when copy fails for %s",
|
||||
fakeAsync((action: string) => {
|
||||
let promptPromise: (val?: unknown) => void;
|
||||
mockCipherService.decrypt.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
...mockCipher,
|
||||
reprompt: CipherRepromptType.Password,
|
||||
}),
|
||||
mockCipherService.cipherViews$.mockImplementationOnce((userId) =>
|
||||
of([
|
||||
{
|
||||
...mockCipher,
|
||||
reprompt: CipherRepromptType.Password,
|
||||
},
|
||||
]),
|
||||
);
|
||||
copy.mockImplementationOnce(() => {
|
||||
return new Promise((resolve) => {
|
||||
@@ -314,7 +320,7 @@ describe("ViewV2Component", () => {
|
||||
});
|
||||
});
|
||||
|
||||
params$.next({ action });
|
||||
params$.next({ action, cipherId: mockCipher.id });
|
||||
|
||||
flush(); // Flush all pending actions
|
||||
|
||||
@@ -336,7 +342,7 @@ describe("ViewV2Component", () => {
|
||||
.spyOn(BrowserApi, "focusTab")
|
||||
.mockImplementation(() => Promise.resolve());
|
||||
|
||||
params$.next({ action: AUTOFILL_ID, senderTabId: 99 });
|
||||
params$.next({ action: AUTOFILL_ID, senderTabId: 99, cipherId: mockCipher.id });
|
||||
|
||||
flush(); // Resolve all promises
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Component } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormsModule } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { firstValueFrom, Observable, switchMap, of } from "rxjs";
|
||||
import { firstValueFrom, Observable, switchMap, of, map } from "rxjs";
|
||||
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
@@ -209,8 +209,12 @@ export class ViewV2Component {
|
||||
}
|
||||
|
||||
async getCipherData(id: string, userId: UserId) {
|
||||
const cipher = await this.cipherService.get(id, userId);
|
||||
return await this.cipherService.decrypt(cipher, userId);
|
||||
return await firstValueFrom(
|
||||
this.cipherService.cipherViews$(userId).pipe(
|
||||
filterOutNullish(),
|
||||
map((ciphers) => ciphers.find((c) => c.id === id)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async editCipher() {
|
||||
|
||||
@@ -1,29 +1,23 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { Router } from "@angular/router";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
import { IntroCarouselService } from "../services/intro-carousel.service";
|
||||
|
||||
import { IntroCarouselGuard } from "./intro-carousel.guard";
|
||||
|
||||
describe("IntroCarouselGuard", () => {
|
||||
let mockConfigService: MockProxy<ConfigService>;
|
||||
const mockIntroCarouselService = {
|
||||
introCarouselState$: of(true),
|
||||
};
|
||||
const createUrlTree = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
mockConfigService = mock<ConfigService>();
|
||||
createUrlTree.mockClear();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: Router, useValue: { createUrlTree } },
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
{
|
||||
provide: IntroCarouselService,
|
||||
useValue: mockIntroCarouselService,
|
||||
@@ -32,22 +26,16 @@ describe("IntroCarouselGuard", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should return true if the feature flag is off", async () => {
|
||||
mockConfigService.getFeatureFlag.mockResolvedValue(false);
|
||||
const result = await TestBed.runInInjectionContext(async () => await IntroCarouselGuard());
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
it("should navigate to intro-carousel route if feature flag is true and dismissed is true", async () => {
|
||||
mockConfigService.getFeatureFlag.mockResolvedValue(true);
|
||||
it("should return true when dismissed is true", async () => {
|
||||
const result = await TestBed.runInInjectionContext(async () => await IntroCarouselGuard());
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should navigate to intro-carousel route if feature flag is true and dismissed is false", async () => {
|
||||
it("should navigate to intro-carousel route when dismissed is false", async () => {
|
||||
TestBed.overrideProvider(IntroCarouselService, {
|
||||
useValue: { introCarouselState$: of(false) },
|
||||
});
|
||||
mockConfigService.getFeatureFlag.mockResolvedValue(true);
|
||||
|
||||
await TestBed.runInInjectionContext(async () => await IntroCarouselGuard());
|
||||
expect(createUrlTree).toHaveBeenCalledWith(["/intro-carousel"]);
|
||||
});
|
||||
|
||||
@@ -2,23 +2,15 @@ import { inject } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
import { IntroCarouselService } from "../services/intro-carousel.service";
|
||||
|
||||
export const IntroCarouselGuard = async () => {
|
||||
const router = inject(Router);
|
||||
const configService = inject(ConfigService);
|
||||
const introCarouselService = inject(IntroCarouselService);
|
||||
|
||||
const hasOnboardingNudgesFlag = await configService.getFeatureFlag(
|
||||
FeatureFlag.PM8851_BrowserOnboardingNudge,
|
||||
);
|
||||
|
||||
const hasIntroCarouselDismissed = await firstValueFrom(introCarouselService.introCarouselState$);
|
||||
|
||||
if (!hasOnboardingNudgesFlag || hasIntroCarouselDismissed) {
|
||||
if (hasIntroCarouselDismissed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { firstValueFrom, map, Observable } from "rxjs";
|
||||
import { map, Observable } from "rxjs";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import {
|
||||
GlobalState,
|
||||
KeyDefinition,
|
||||
@@ -28,17 +26,9 @@ export class IntroCarouselService {
|
||||
map((x) => x ?? false),
|
||||
);
|
||||
|
||||
constructor(
|
||||
private stateProvider: StateProvider,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
constructor(private stateProvider: StateProvider) {}
|
||||
|
||||
async setIntroCarouselDismissed(): Promise<void> {
|
||||
const hasVaultNudgeFlag = await firstValueFrom(
|
||||
this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge),
|
||||
);
|
||||
if (hasVaultNudgeFlag) {
|
||||
await this.introCarouselState.update(() => true);
|
||||
}
|
||||
await this.introCarouselState.update(() => true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +125,10 @@ describe("VaultPopupAutofillService", () => {
|
||||
});
|
||||
|
||||
it("should only fetch the current tab once when subscribed to multiple times", async () => {
|
||||
(BrowserApi.getTabFromCurrentWindow as jest.Mock).mockClear();
|
||||
|
||||
service.refreshCurrentTab();
|
||||
|
||||
const firstTracked = subscribeTo(service.currentAutofillTab$);
|
||||
const secondTracked = subscribeTo(service.currentAutofillTab$);
|
||||
|
||||
@@ -195,6 +199,7 @@ describe("VaultPopupAutofillService", () => {
|
||||
|
||||
// Refresh the current tab so the mockedPageDetails$ are used
|
||||
service.refreshCurrentTab();
|
||||
(service as any)._currentPageDetails$ = of(mockPageDetails);
|
||||
});
|
||||
|
||||
describe("doAutofill()", () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Injectable } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import {
|
||||
combineLatest,
|
||||
debounceTime,
|
||||
firstValueFrom,
|
||||
map,
|
||||
Observable,
|
||||
@@ -164,6 +165,7 @@ export class VaultPopupAutofillService {
|
||||
}),
|
||||
);
|
||||
}),
|
||||
debounceTime(50),
|
||||
shareReplay({ refCount: false, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { firstValueFrom, map, switchMap } from "rxjs";
|
||||
|
||||
import {
|
||||
OrganizationUserApiService,
|
||||
OrganizationUserConfirmRequest,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { EncString } from "@bitwarden/sdk-internal";
|
||||
@@ -24,6 +27,7 @@ export class ConfirmCommand {
|
||||
private keyService: KeyService,
|
||||
private encryptService: EncryptService,
|
||||
private organizationUserApiService: OrganizationUserApiService,
|
||||
private accountService: AccountService,
|
||||
private configService: ConfigService,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
@@ -53,7 +57,14 @@ export class ConfirmCommand {
|
||||
return Response.badRequest("`" + options.organizationId + "` is not a GUID.");
|
||||
}
|
||||
try {
|
||||
const orgKey = await this.keyService.getOrgKey(options.organizationId);
|
||||
const orgKey = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.keyService.orgKeys$(userId)),
|
||||
map((orgKeys) => orgKeys[options.organizationId as OrganizationId] ?? null),
|
||||
),
|
||||
);
|
||||
|
||||
if (orgKey == null) {
|
||||
throw new Error("No encryption key for this organization.");
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ export abstract class BaseProgram {
|
||||
if (!userId) {
|
||||
fail();
|
||||
}
|
||||
const authed = await this.serviceContainer.stateService.getIsAuthenticated({ userId });
|
||||
const authed = await firstValueFrom(this.serviceContainer.tokenService.hasAccessToken$(userId));
|
||||
if (!authed) {
|
||||
fail();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { firstValueFrom, map, switchMap } from "rxjs";
|
||||
|
||||
import { CollectionRequest } from "@bitwarden/admin-console/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
@@ -14,6 +14,7 @@ import { CipherExport } from "@bitwarden/common/models/export/cipher.export";
|
||||
import { CollectionExport } from "@bitwarden/common/models/export/collection.export";
|
||||
import { FolderExport } from "@bitwarden/common/models/export/folder.export";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
@@ -201,7 +202,13 @@ export class EditCommand {
|
||||
return Response.badRequest("`organizationid` option does not match request object.");
|
||||
}
|
||||
try {
|
||||
const orgKey = await this.keyService.getOrgKey(req.organizationId);
|
||||
const orgKey = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.keyService.orgKeys$(userId)),
|
||||
map((orgKeys) => orgKeys[options.organizationId as OrganizationId] ?? null),
|
||||
),
|
||||
);
|
||||
if (orgKey == null) {
|
||||
throw new Error("No encryption key for this organization.");
|
||||
}
|
||||
@@ -218,14 +225,15 @@ export class EditCommand {
|
||||
: req.users.map(
|
||||
(u) => new SelectionReadOnlyRequest(u.id, u.readOnly, u.hidePasswords, u.manage),
|
||||
);
|
||||
const request = new CollectionRequest();
|
||||
request.name = (await this.encryptService.encryptString(req.name, orgKey)).encryptedString;
|
||||
request.externalId = req.externalId;
|
||||
request.groups = groups;
|
||||
request.users = users;
|
||||
const request = new CollectionRequest({
|
||||
name: await this.encryptService.encryptString(req.name, orgKey),
|
||||
externalId: req.externalId,
|
||||
users,
|
||||
groups,
|
||||
});
|
||||
|
||||
const response = await this.apiService.putCollection(req.organizationId, id, request);
|
||||
const view = CollectionExport.toView(req);
|
||||
view.id = response.id;
|
||||
const view = CollectionExport.toView(req, response.id);
|
||||
const res = new OrganizationCollectionResponse(view, groups, users);
|
||||
return Response.success(res);
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
import { firstValueFrom, map, switchMap } from "rxjs";
|
||||
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
@@ -13,7 +13,6 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { CardExport } from "@bitwarden/common/models/export/card.export";
|
||||
import { CipherExport } from "@bitwarden/common/models/export/cipher.export";
|
||||
import { CollectionExport } from "@bitwarden/common/models/export/collection.export";
|
||||
@@ -452,6 +451,7 @@ export class GetCommand extends DownloadCommand {
|
||||
const orgKeys = await firstValueFrom(this.keyService.activeUserOrgKeys$);
|
||||
decCollection = await collection.decrypt(
|
||||
orgKeys[collection.organizationId as OrganizationId],
|
||||
this.encryptService,
|
||||
);
|
||||
}
|
||||
} else if (id.trim() !== "") {
|
||||
@@ -485,15 +485,21 @@ export class GetCommand extends DownloadCommand {
|
||||
return Response.badRequest("`" + options.organizationId + "` is not a GUID.");
|
||||
}
|
||||
try {
|
||||
const orgKey = await this.keyService.getOrgKey(options.organizationId);
|
||||
const orgKey = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.keyService.orgKeys$(userId)),
|
||||
map((orgKeys) => orgKeys[options.organizationId as OrganizationId] ?? null),
|
||||
),
|
||||
);
|
||||
if (orgKey == null) {
|
||||
throw new Error("No encryption key for this organization.");
|
||||
}
|
||||
|
||||
const response = await this.apiService.getCollectionAccessDetails(options.organizationId, id);
|
||||
const decCollection = new CollectionView(response);
|
||||
decCollection.name = await this.encryptService.decryptString(
|
||||
new EncString(response.name),
|
||||
const decCollection = await CollectionView.fromCollectionAccessDetails(
|
||||
response,
|
||||
this.encryptService,
|
||||
orgKey,
|
||||
);
|
||||
const groups =
|
||||
|
||||
@@ -211,7 +211,9 @@ export class ListCommand {
|
||||
}
|
||||
const collections = response.data
|
||||
.filter((c) => c.organizationId === options.organizationId)
|
||||
.map((r) => new Collection(new CollectionData(r as ApiCollectionDetailsResponse)));
|
||||
.map((r) =>
|
||||
Collection.fromCollectionData(new CollectionData(r as ApiCollectionDetailsResponse)),
|
||||
);
|
||||
const orgKeys = await firstValueFrom(this.keyService.orgKeys$(userId));
|
||||
if (orgKeys == null) {
|
||||
throw new Error("Organization keys not found.");
|
||||
|
||||
@@ -107,7 +107,8 @@ export class OssServeConfigurator {
|
||||
);
|
||||
this.generateCommand = new GenerateCommand(
|
||||
this.serviceContainer.passwordGenerationService,
|
||||
this.serviceContainer.stateService,
|
||||
this.serviceContainer.tokenService,
|
||||
this.serviceContainer.accountService,
|
||||
);
|
||||
this.syncCommand = new SyncCommand(this.serviceContainer.syncService);
|
||||
this.statusCommand = new StatusCommand(
|
||||
@@ -131,6 +132,7 @@ export class OssServeConfigurator {
|
||||
this.serviceContainer.keyService,
|
||||
this.serviceContainer.encryptService,
|
||||
this.serviceContainer.organizationUserApiService,
|
||||
this.serviceContainer.accountService,
|
||||
this.serviceContainer.configService,
|
||||
this.serviceContainer.i18nService,
|
||||
);
|
||||
@@ -416,14 +418,18 @@ export class OssServeConfigurator {
|
||||
}
|
||||
|
||||
protected async errorIfLocked(res: koa.Response) {
|
||||
const authed = await this.serviceContainer.stateService.getIsAuthenticated();
|
||||
const userId = await firstValueFrom(
|
||||
this.serviceContainer.accountService.activeAccount$.pipe(map((account) => account?.id)),
|
||||
);
|
||||
|
||||
const authed =
|
||||
userId != null ||
|
||||
(await firstValueFrom(this.serviceContainer.tokenService.hasAccessToken$(userId)));
|
||||
|
||||
if (!authed) {
|
||||
this.processResponse(res, Response.error("You are not logged in."));
|
||||
return true;
|
||||
}
|
||||
const userId = await firstValueFrom(
|
||||
this.serviceContainer.accountService.activeAccount$.pipe(map((account) => account?.id)),
|
||||
);
|
||||
if (await this.serviceContainer.keyService.hasUserKey(userId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// @ts-strict-ignore
|
||||
import * as chalk from "chalk";
|
||||
import { program, Command, OptionValues } from "commander";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { firstValueFrom, of, switchMap } from "rxjs";
|
||||
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
|
||||
@@ -129,7 +129,17 @@ export class Program extends BaseProgram {
|
||||
"Path to a file containing your password as its first line",
|
||||
)
|
||||
.option("--check", "Check login status.", async () => {
|
||||
const authed = await this.serviceContainer.stateService.getIsAuthenticated();
|
||||
const authed = await firstValueFrom(
|
||||
this.serviceContainer.accountService.activeAccount$.pipe(
|
||||
switchMap((account) => {
|
||||
if (account == null) {
|
||||
return of(false);
|
||||
}
|
||||
|
||||
return this.serviceContainer.tokenService.hasAccessToken$(account.id);
|
||||
}),
|
||||
),
|
||||
);
|
||||
if (authed) {
|
||||
const res = new MessageResponse("You are logged in!", null);
|
||||
this.processResponse(Response.success(res), true);
|
||||
@@ -350,7 +360,8 @@ export class Program extends BaseProgram {
|
||||
.action(async (options) => {
|
||||
const command = new GenerateCommand(
|
||||
this.serviceContainer.passwordGenerationService,
|
||||
this.serviceContainer.stateService,
|
||||
this.serviceContainer.tokenService,
|
||||
this.serviceContainer.accountService,
|
||||
);
|
||||
const response = await command.run(options);
|
||||
this.processResponse(response);
|
||||
|
||||
@@ -60,6 +60,10 @@ import {
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import {
|
||||
DefaultKeyGenerationService,
|
||||
KeyGenerationService,
|
||||
} from "@bitwarden/common/key-management/crypto";
|
||||
import { EncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/encrypt.service.implementation";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { DeviceTrustService } from "@bitwarden/common/key-management/device-trust/services/device-trust.service.implementation";
|
||||
@@ -81,14 +85,10 @@ import {
|
||||
EnvironmentService,
|
||||
RegionConfig,
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
import { KeySuffixOptions, LogLevelType } from "@bitwarden/common/platform/enums";
|
||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||
import { MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
import { Account } from "@bitwarden/common/platform/models/domain/account";
|
||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||
import {
|
||||
TaskSchedulerService,
|
||||
DefaultTaskSchedulerService,
|
||||
@@ -99,23 +99,23 @@ import { DefaultConfigService } from "@bitwarden/common/platform/services/config
|
||||
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
|
||||
import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service";
|
||||
import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
|
||||
import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service";
|
||||
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
||||
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||
import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory";
|
||||
import { DefaultSdkService } from "@bitwarden/common/platform/services/sdk/default-sdk.service";
|
||||
import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory";
|
||||
import { StateService } from "@bitwarden/common/platform/services/state.service";
|
||||
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
|
||||
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
|
||||
import {
|
||||
ActiveUserStateProvider,
|
||||
DefaultStateService,
|
||||
DerivedStateProvider,
|
||||
GlobalStateProvider,
|
||||
SingleUserStateProvider,
|
||||
StateEventRunnerService,
|
||||
StateProvider,
|
||||
StateService,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
/* eslint-disable import/no-restricted-paths -- We need the implementation to inject, but generally these should not be accessed */
|
||||
import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-active-user-state.provider";
|
||||
@@ -210,6 +210,7 @@ export class ServiceContainer {
|
||||
secureStorageService: NodeEnvSecureStorageService;
|
||||
memoryStorageService: MemoryStorageService;
|
||||
memoryStorageForStateProviders: MemoryStorageServiceForStateProviders;
|
||||
migrationRunner: MigrationRunner;
|
||||
i18nService: I18nService;
|
||||
platformUtilsService: CliPlatformUtilsService;
|
||||
keyService: KeyService;
|
||||
@@ -239,7 +240,7 @@ export class ServiceContainer {
|
||||
individualExportService: IndividualVaultExportServiceAbstraction;
|
||||
organizationExportService: OrganizationVaultExportServiceAbstraction;
|
||||
searchService: SearchService;
|
||||
keyGenerationService: KeyGenerationServiceAbstraction;
|
||||
keyGenerationService: KeyGenerationService;
|
||||
cryptoFunctionService: NodeCryptoFunctionService;
|
||||
encryptService: EncryptServiceImplementation;
|
||||
authService: AuthService;
|
||||
@@ -377,8 +378,10 @@ export class ServiceContainer {
|
||||
this.singleUserStateProvider,
|
||||
);
|
||||
|
||||
const activeUserAccessor = new DefaultActiveUserAccessor(this.accountService);
|
||||
|
||||
this.activeUserStateProvider = new DefaultActiveUserStateProvider(
|
||||
new DefaultActiveUserAccessor(this.accountService),
|
||||
activeUserAccessor,
|
||||
this.singleUserStateProvider,
|
||||
);
|
||||
|
||||
@@ -397,7 +400,7 @@ export class ServiceContainer {
|
||||
process.env.ADDITIONAL_REGIONS as unknown as RegionConfig[],
|
||||
);
|
||||
|
||||
this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService);
|
||||
this.keyGenerationService = new DefaultKeyGenerationService(this.cryptoFunctionService);
|
||||
|
||||
this.tokenService = new TokenService(
|
||||
this.singleUserStateProvider,
|
||||
@@ -410,23 +413,17 @@ export class ServiceContainer {
|
||||
logoutCallback,
|
||||
);
|
||||
|
||||
const migrationRunner = new MigrationRunner(
|
||||
this.migrationRunner = new MigrationRunner(
|
||||
this.storageService,
|
||||
this.logService,
|
||||
new MigrationBuilderService(),
|
||||
ClientType.Cli,
|
||||
);
|
||||
|
||||
this.stateService = new StateService(
|
||||
this.stateService = new DefaultStateService(
|
||||
this.storageService,
|
||||
this.secureStorageService,
|
||||
this.memoryStorageService,
|
||||
this.logService,
|
||||
new StateFactory(GlobalState, Account),
|
||||
this.accountService,
|
||||
this.environmentService,
|
||||
this.tokenService,
|
||||
migrationRunner,
|
||||
activeUserAccessor,
|
||||
);
|
||||
|
||||
this.kdfConfigService = new DefaultKdfConfigService(this.stateProvider);
|
||||
@@ -530,10 +527,7 @@ export class ServiceContainer {
|
||||
this.authService,
|
||||
);
|
||||
|
||||
this.domainSettingsService = new DefaultDomainSettingsService(
|
||||
this.stateProvider,
|
||||
this.configService,
|
||||
);
|
||||
this.domainSettingsService = new DefaultDomainSettingsService(this.stateProvider);
|
||||
|
||||
this.fileUploadService = new FileUploadService(this.logService, this.apiService);
|
||||
|
||||
@@ -714,7 +708,6 @@ export class ServiceContainer {
|
||||
this.apiService,
|
||||
this.i18nService,
|
||||
this.searchService,
|
||||
this.stateService,
|
||||
this.autofillSettingsService,
|
||||
this.encryptService,
|
||||
this.cipherFileUploadService,
|
||||
@@ -765,6 +758,7 @@ export class ServiceContainer {
|
||||
this.messagingService,
|
||||
this.searchService,
|
||||
this.stateService,
|
||||
this.tokenService,
|
||||
this.authService,
|
||||
this.vaultTimeoutSettingsService,
|
||||
this.stateEventRunnerService,
|
||||
@@ -791,7 +785,6 @@ export class ServiceContainer {
|
||||
this.sendService,
|
||||
this.logService,
|
||||
this.keyConnectorService,
|
||||
this.stateService,
|
||||
this.providerService,
|
||||
this.folderApiService,
|
||||
this.organizationService,
|
||||
@@ -904,7 +897,8 @@ export class ServiceContainer {
|
||||
|
||||
await this.stateEventRunnerService.handleEvent("logout", userId as UserId);
|
||||
|
||||
await this.stateService.clean();
|
||||
await this.stateService.clean({ userId: userId });
|
||||
await this.tokenService.clearAccessToken(userId);
|
||||
await this.accountService.clean(userId as UserId);
|
||||
await this.accountService.switchAccount(null);
|
||||
process.env.BW_SESSION = undefined;
|
||||
@@ -918,7 +912,8 @@ export class ServiceContainer {
|
||||
|
||||
await this.sdkLoadService.loadAndInit();
|
||||
await this.storageService.init();
|
||||
await this.stateService.init();
|
||||
|
||||
await this.migrationRunner.run();
|
||||
this.containerService.attachToGlobal(global);
|
||||
await this.i18nService.init();
|
||||
this.twoFactorService.init();
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { firstValueFrom, of, switchMap } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import {
|
||||
DefaultPasswordGenerationOptions,
|
||||
DefaultPassphraseGenerationOptions,
|
||||
@@ -17,7 +20,8 @@ import { CliUtils } from "../utils";
|
||||
export class GenerateCommand {
|
||||
constructor(
|
||||
private passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
private stateService: StateService,
|
||||
private tokenService: TokenService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
async run(cmdOptions: Record<string, any>): Promise<Response> {
|
||||
@@ -38,7 +42,18 @@ export class GenerateCommand {
|
||||
ambiguous: !normalizedOptions.ambiguous,
|
||||
};
|
||||
|
||||
const enforcedOptions = (await this.stateService.getIsAuthenticated())
|
||||
const shouldEnforceOptions = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
switchMap((account) => {
|
||||
if (account == null) {
|
||||
return of(false);
|
||||
}
|
||||
|
||||
return this.tokenService.hasAccessToken$(account.id);
|
||||
}),
|
||||
),
|
||||
);
|
||||
const enforcedOptions = shouldEnforceOptions
|
||||
? (await this.passwordGenerationService.enforcePasswordGeneratorPoliciesOnOptions(options))[0]
|
||||
: options;
|
||||
|
||||
|
||||
@@ -432,6 +432,7 @@ export class VaultProgram extends BaseProgram {
|
||||
this.serviceContainer.keyService,
|
||||
this.serviceContainer.encryptService,
|
||||
this.serviceContainer.organizationUserApiService,
|
||||
this.serviceContainer.accountService,
|
||||
this.serviceContainer.configService,
|
||||
this.serviceContainer.i18nService,
|
||||
);
|
||||
|
||||
@@ -233,14 +233,14 @@ export class CreateCommand {
|
||||
: req.users.map(
|
||||
(u) => new SelectionReadOnlyRequest(u.id, u.readOnly, u.hidePasswords, u.manage),
|
||||
);
|
||||
const request = new CollectionRequest();
|
||||
request.name = (await this.encryptService.encryptString(req.name, orgKey)).encryptedString;
|
||||
request.externalId = req.externalId;
|
||||
request.groups = groups;
|
||||
request.users = users;
|
||||
const request = new CollectionRequest({
|
||||
name: await this.encryptService.encryptString(req.name, orgKey),
|
||||
externalId: req.externalId,
|
||||
groups,
|
||||
users,
|
||||
});
|
||||
const response = await this.apiService.postCollection(req.organizationId, request);
|
||||
const view = CollectionExport.toView(req);
|
||||
view.id = response.id;
|
||||
const view = CollectionExport.toView(req, response.id);
|
||||
const res = new OrganizationCollectionResponse(view, groups, users);
|
||||
return Response.success(res);
|
||||
} catch (e) {
|
||||
|
||||
@@ -2,10 +2,7 @@ import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { AuthenticationTimeoutComponent } from "@bitwarden/angular/auth/components/authentication-timeout.component";
|
||||
import {
|
||||
DesktopDefaultOverlayPosition,
|
||||
EnvironmentSelectorComponent,
|
||||
} from "@bitwarden/angular/auth/components/environment-selector.component";
|
||||
import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/environment-selector/environment-selector.component";
|
||||
import {
|
||||
authGuard,
|
||||
lockGuard,
|
||||
@@ -174,9 +171,6 @@ const routes: Routes = [
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
data: {
|
||||
overlayPosition: DesktopDefaultOverlayPosition,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -205,9 +199,6 @@ const routes: Routes = [
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
data: {
|
||||
overlayPosition: DesktopDefaultOverlayPosition,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -228,9 +219,6 @@ const routes: Routes = [
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
data: {
|
||||
overlayPosition: DesktopDefaultOverlayPosition,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -265,9 +253,6 @@ const routes: Routes = [
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
data: {
|
||||
overlayPosition: DesktopDefaultOverlayPosition,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -40,6 +40,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
|
||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
@@ -175,6 +176,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
private readonly destroyRef: DestroyRef,
|
||||
private readonly documentLangSetter: DocumentLangSetter,
|
||||
private restrictedItemTypesService: RestrictedItemTypesService,
|
||||
private readonly tokenService: TokenService,
|
||||
) {
|
||||
this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe();
|
||||
|
||||
@@ -684,6 +686,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut);
|
||||
|
||||
await this.stateService.clean({ userId: userBeingLoggedOut });
|
||||
await this.tokenService.clearAccessToken(userBeingLoggedOut);
|
||||
await this.accountService.clean(userBeingLoggedOut);
|
||||
|
||||
// HACK: Wait for the user logging outs authentication status to transition to LoggedOut
|
||||
|
||||
@@ -15,6 +15,7 @@ import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-
|
||||
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { NotificationsService } from "@bitwarden/common/platform/notifications";
|
||||
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
|
||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
|
||||
import { SyncService as SyncServiceAbstraction } from "@bitwarden/common/platform/sync";
|
||||
import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service";
|
||||
@@ -52,6 +53,7 @@ export class InitService {
|
||||
private autotypeService: DesktopAutotypeService,
|
||||
private sdkLoadService: SdkLoadService,
|
||||
@Inject(DOCUMENT) private document: Document,
|
||||
private readonly migrationRunner: MigrationRunner,
|
||||
) {}
|
||||
|
||||
init() {
|
||||
@@ -59,7 +61,7 @@ export class InitService {
|
||||
await this.sdkLoadService.loadAndInit();
|
||||
await this.sshAgentService.init();
|
||||
this.nativeMessagingService.init();
|
||||
await this.stateService.init({ runMigrations: false }); // Desktop will run them in main process
|
||||
await this.migrationRunner.waitForCompletion(); // Desktop will run migrations in the main process
|
||||
|
||||
const accounts = await firstValueFrom(this.accountService.accounts$);
|
||||
const setUserKeyInMemoryPromises = [];
|
||||
|
||||
@@ -51,6 +51,7 @@ import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/
|
||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
|
||||
import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { WebCryptoFunctionService } from "@bitwarden/common/key-management/crypto/services/web-crypto-function.service";
|
||||
@@ -67,7 +68,6 @@ import { Fido2AuthenticatorService as Fido2AuthenticatorServiceAbstraction } fro
|
||||
import { Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-user-interface.service.abstraction";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||
import {
|
||||
LogService,
|
||||
LogService as LogServiceAbstraction,
|
||||
@@ -304,7 +304,7 @@ const safeProviders: SafeProvider[] = [
|
||||
deps: [
|
||||
PinServiceAbstraction,
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
KeyGenerationServiceAbstraction,
|
||||
KeyGenerationService,
|
||||
CryptoFunctionServiceAbstraction,
|
||||
EncryptService,
|
||||
PlatformUtilsServiceAbstraction,
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
|
||||
import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component";
|
||||
|
||||
import { SharedModule } from "../../app/shared/shared.module";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule, RouterModule],
|
||||
declarations: [EnvironmentSelectorComponent],
|
||||
exports: [],
|
||||
})
|
||||
export class LoginModule {}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
@@ -65,7 +65,7 @@ export class ElectronKeyService extends DefaultKeyService {
|
||||
|
||||
protected override async getKeyFromStorage(
|
||||
keySuffix: KeySuffixOptions,
|
||||
userId?: UserId,
|
||||
userId: UserId,
|
||||
): Promise<UserKey | null> {
|
||||
return await super.getKeyFromStorage(keySuffix, userId);
|
||||
}
|
||||
|
||||
@@ -1226,7 +1226,7 @@
|
||||
"message": "Prijava dvostrukom autentifikacijom"
|
||||
},
|
||||
"vaultTimeoutHeader": {
|
||||
"message": "Vault timeout"
|
||||
"message": "Istek trezora"
|
||||
},
|
||||
"vaultTimeout": {
|
||||
"message": "Istek trezora"
|
||||
@@ -1235,7 +1235,7 @@
|
||||
"message": "Vrijeme isteka"
|
||||
},
|
||||
"vaultTimeoutAction1": {
|
||||
"message": "Timeout action"
|
||||
"message": "Radnja nakon isteka"
|
||||
},
|
||||
"vaultTimeoutDesc": {
|
||||
"message": "Odaberi kada će isteći trezor i koja će se radnja izvršiti."
|
||||
@@ -2518,10 +2518,10 @@
|
||||
"message": "Vrijeme isteka premašuje ograničenje koju je postavila tvoja organizacija."
|
||||
},
|
||||
"vaultTimeoutPolicyAffectingOptions": {
|
||||
"message": "Enterprise policy requirements have been applied to your timeout options"
|
||||
"message": "Pravila tvrtke primijenjena su na vrijeme isteka"
|
||||
},
|
||||
"vaultTimeoutPolicyInEffect": {
|
||||
"message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).",
|
||||
"message": "Pravilo tvoje organizacije utječe na istek trezora. Najveće dozvoljeno vrijeme isteka je $HOURS$:$MINUTES$ h.",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
@@ -2534,7 +2534,7 @@
|
||||
}
|
||||
},
|
||||
"vaultTimeoutPolicyMaximumError": {
|
||||
"message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum",
|
||||
"message": "Tvoja organizacija je zadano postavila kraće vrijeme isteka. Najviše: $HOURS$:$MINUTES$",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
@@ -3037,7 +3037,7 @@
|
||||
}
|
||||
},
|
||||
"loginRequestApprovedForEmailOnDevice": {
|
||||
"message": "Login request approved for $EMAIL$ on $DEVICE$",
|
||||
"message": "Prijava za $EMAIL$ potvrđena na uređaju $DEVICE$",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
@@ -3050,17 +3050,17 @@
|
||||
}
|
||||
},
|
||||
"youDeniedLoginAttemptFromAnotherDevice": {
|
||||
"message": "You denied a login attempt from another device. If this was you, try to log in with the device again."
|
||||
"message": "Odbijena je prijava na drugom uređaju. Ako si ovo stvarno ti, pokušaj se ponovno prijaviti uređajem."
|
||||
},
|
||||
"webApp": {
|
||||
"message": "Web app"
|
||||
"message": "Web aplikacija"
|
||||
},
|
||||
"mobile": {
|
||||
"message": "Mobile",
|
||||
"message": "Mobitel",
|
||||
"description": "Mobile app"
|
||||
},
|
||||
"extension": {
|
||||
"message": "Extension",
|
||||
"message": "Proširenje",
|
||||
"description": "Browser extension/addon"
|
||||
},
|
||||
"desktop": {
|
||||
@@ -3075,10 +3075,10 @@
|
||||
"description": "Software Development Kit"
|
||||
},
|
||||
"server": {
|
||||
"message": "Server"
|
||||
"message": "Poslužitelj"
|
||||
},
|
||||
"loginRequest": {
|
||||
"message": "Login request"
|
||||
"message": "Zahtjev za prijavu"
|
||||
},
|
||||
"deviceType": {
|
||||
"message": "Vrsta uređaja"
|
||||
@@ -4067,10 +4067,10 @@
|
||||
}
|
||||
},
|
||||
"showMore": {
|
||||
"message": "Show more"
|
||||
"message": "Prikaži više"
|
||||
},
|
||||
"showLess": {
|
||||
"message": "Show less"
|
||||
"message": "Pokaži manje"
|
||||
},
|
||||
"enableAutotype": {
|
||||
"message": "Omogući automatski unos"
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
"message": "Pielikumi"
|
||||
},
|
||||
"viewItem": {
|
||||
"message": "Skatīt vienumu"
|
||||
"message": "Apskatīt vienumu"
|
||||
},
|
||||
"name": {
|
||||
"message": "Nosaukums"
|
||||
@@ -573,7 +573,7 @@
|
||||
"message": "Ievietot vietrādi starpliktuvē"
|
||||
},
|
||||
"copyVerificationCodeTotp": {
|
||||
"message": "Ievietot Apliecinājuma kodu (TOTP) starpliktuvē"
|
||||
"message": "Ievietot apliecinājuma kodu (TOTP) starpliktuvē"
|
||||
},
|
||||
"copyFieldCipherName": {
|
||||
"message": "Ievietot starpliktuvē $FIELD$, $CIPHERNAME$",
|
||||
@@ -955,10 +955,10 @@
|
||||
"message": "Ievieto savu drošības atslēgu datora USB ligzdā! Ja tai ir poga, pieskaries tai!"
|
||||
},
|
||||
"recoveryCodeDesc": {
|
||||
"message": "Zaudēta piekļuve visiem divpakāpju nodrošinātājiem? Izmanto atkopšanas kodus, lai atspējotu visus sava konta divpakāpju nodrošinātājus!"
|
||||
"message": "Zaudēta piekļuve visiem divpakāpju nodrošinātājiem? Izmanto atkopes kodus, lai atspējotu visus sava konta divpakāpju nodrošinātājus!"
|
||||
},
|
||||
"recoveryCodeTitle": {
|
||||
"message": "Atgūšanas kods"
|
||||
"message": "Atkopes kods"
|
||||
},
|
||||
"authenticatorAppTitle": {
|
||||
"message": "Autentificētāja lietotne"
|
||||
@@ -2425,7 +2425,7 @@
|
||||
"message": "Pēc savas paroles nomainīšanas būs nepieciešams pieteikties ar jauno paroli. Spēkā esošajās sesijās citās ierīcēs stundas laikā notiks atteikšanās."
|
||||
},
|
||||
"accountRecoveryUpdateMasterPasswordSubtitle": {
|
||||
"message": "Jānomaina sava galvenā'parole, lai pabeigtu konta atkopi."
|
||||
"message": "Jānomaina sava galvenā parole, lai pabeigtu konta atkopi."
|
||||
},
|
||||
"updateMasterPasswordSubtitle": {
|
||||
"message": "Galvenā parole neatbilst šīs apvienības prasībām. Jānomaina sava galvenā parole, lai turpinātu."
|
||||
@@ -3015,10 +3015,10 @@
|
||||
"message": "Ir jāuzstāda pieteikšanās ar ierīci Bitwarden lietotnes iestatījumos. Nepieciešama cita iespēja?"
|
||||
},
|
||||
"viewAllLogInOptions": {
|
||||
"message": "Skatīt visas pieteikšanās iespējas"
|
||||
"message": "Apskatīt visas pieteikšanās iespējas"
|
||||
},
|
||||
"viewAllLoginOptions": {
|
||||
"message": "Skatīt visas pieteikšanās iespējas"
|
||||
"message": "Apskatīt visas pieteikšanās iespējas"
|
||||
},
|
||||
"resendNotification": {
|
||||
"message": "Atkārtoti nosūtīt paziņojumu"
|
||||
@@ -3241,7 +3241,7 @@
|
||||
"message": "pašmitināts"
|
||||
},
|
||||
"accessDenied": {
|
||||
"message": "Piekļuve liegta. Nav nepieciešamo atļauju, lai skatītu šo lapu."
|
||||
"message": "Piekļuve liegta. Nav nepieciešamo atļauju, lai apskatītu šo lapu."
|
||||
},
|
||||
"accountSuccessfullyCreated": {
|
||||
"message": "Konts ir veiksmīgi izveidots."
|
||||
@@ -3627,7 +3627,7 @@
|
||||
"description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy"
|
||||
},
|
||||
"startsWithAdvancedOptionWarning": {
|
||||
"message": "\"Sākas ar' ir lietpratējiem paredzēta iespēja ar paaugstinātu piekļuves datu atklāšanas bīstamību.",
|
||||
"message": "“Sākas ar” ir lietpratējiem paredzēta iespēja ar paaugstinātu piekļuves datu atklāšanas bīstamību.",
|
||||
"description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy"
|
||||
},
|
||||
"uriMatchWarningDialogLink": {
|
||||
@@ -3667,7 +3667,7 @@
|
||||
"message": "Kļūda mērķa mapes piešķiršanā."
|
||||
},
|
||||
"viewItemsIn": {
|
||||
"message": "Skatīt $NAME$ vienumus",
|
||||
"message": "Apskatīt $NAME$ vienumus",
|
||||
"description": "Button to view the contents of a folder or collection",
|
||||
"placeholders": {
|
||||
"name": {
|
||||
@@ -3820,7 +3820,7 @@
|
||||
"message": "Atļaut ekrāna tveršanu"
|
||||
},
|
||||
"allowScreenshotsDesc": {
|
||||
"message": "Ļaut Bitwarden darbvirsmas lietotni tvert ekrānuzņēmumos un rādīt attālās darbvirsmas sesijās. Atspējošana liegs piekļuvu atsevišķos ārējos ekrānos."
|
||||
"message": "Ļaut Bitwarden darbvirsmas lietotni tvert ekrānuzņēmumos un rādīt attālās darbvirsmas sesijās. Atspējošana liegs piekļuvi atsevišķos ārējos ekrānos."
|
||||
},
|
||||
"confirmWindowStillVisibleTitle": {
|
||||
"message": "Apstirpināt, ka logs joprojām ir redzams"
|
||||
|
||||
@@ -3171,7 +3171,7 @@
|
||||
"message": "Ważne:"
|
||||
},
|
||||
"accessing": {
|
||||
"message": "Uzyskiwanie dostępu"
|
||||
"message": "Serwer"
|
||||
},
|
||||
"accessTokenUnableToBeDecrypted": {
|
||||
"message": "Zostałeś wylogowany, ponieważ token dostępu nie mógł zostać odszyfrowany. Zaloguj się ponownie, aby rozwiązać ten problem."
|
||||
|
||||
@@ -3623,7 +3623,7 @@
|
||||
"description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item."
|
||||
},
|
||||
"regExAdvancedOptionWarning": {
|
||||
"message": "A \"expressão regular\" é uma opção avançada com um risco acrescido de exposição de credenciais.",
|
||||
"message": "A \"Expressão regular\" é uma opção avançada com um risco acrescido de exposição de credenciais.",
|
||||
"description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy"
|
||||
},
|
||||
"startsWithAdvancedOptionWarning": {
|
||||
|
||||
@@ -2518,10 +2518,10 @@
|
||||
"message": "Ditt valvs tid för timeout överskrider de begränsningar som fastställts av din organisation."
|
||||
},
|
||||
"vaultTimeoutPolicyAffectingOptions": {
|
||||
"message": "Enterprise policy requirements have been applied to your timeout options"
|
||||
"message": "Företagets policykrav har tillämpats på dina tidsgränsalternativ"
|
||||
},
|
||||
"vaultTimeoutPolicyInEffect": {
|
||||
"message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).",
|
||||
"message": "Din organisations policy har fastställt den maximalt tillåtna tidsgränsen för valvet till $HOURS$ timmar och $MINUTES$ minuter.",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
@@ -2534,7 +2534,7 @@
|
||||
}
|
||||
},
|
||||
"vaultTimeoutPolicyMaximumError": {
|
||||
"message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum",
|
||||
"message": "Tidsgränsen överskrider den begränsning som din organisation har ställt in: $HOURS$ timmar och $MINUTES$ minut(er) maximalt",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
|
||||
@@ -324,7 +324,7 @@
|
||||
"message": "Tháng 12"
|
||||
},
|
||||
"ex": {
|
||||
"message": "Ví dụ:",
|
||||
"message": "ví dụ.",
|
||||
"description": "Short abbreviation for 'example'."
|
||||
},
|
||||
"title": {
|
||||
@@ -1390,7 +1390,7 @@
|
||||
"description": "Copy to clipboard"
|
||||
},
|
||||
"checkForUpdates": {
|
||||
"message": "Kiểm tra cập nhật mới"
|
||||
"message": "Kiểm tra cập nhật…"
|
||||
},
|
||||
"version": {
|
||||
"message": "Phiên bản $VERSION_NUM$",
|
||||
@@ -3769,7 +3769,7 @@
|
||||
"message": "Tải lên"
|
||||
},
|
||||
"authorize": {
|
||||
"message": "Uỷ quyền"
|
||||
"message": "Ủy quyền"
|
||||
},
|
||||
"deny": {
|
||||
"message": "Từ chối"
|
||||
|
||||
@@ -1,5 +1 @@
|
||||
@import "../../../../libs/components/src/tw-theme.css";
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
<i class="bwi bwi-fw bwi-business" aria-hidden="true"></i>
|
||||
{{ organization.name }}
|
||||
</button>
|
||||
<span *ngIf="!organization.enabled" class="ml-auto">
|
||||
<span *ngIf="!organization.enabled" class="tw-ml-auto">
|
||||
<i
|
||||
class="bwi bwi-fw bwi-exclamation-triangle text-danger mr-auto"
|
||||
attr.aria-label="{{ 'organizationIsDisabled' | i18n }}"
|
||||
@@ -124,7 +124,7 @@
|
||||
<i class="bwi bwi-fw bwi-business" aria-hidden="true"></i>
|
||||
{{ organization.name }}
|
||||
</button>
|
||||
<span *ngIf="!organization.enabled" class="ml-auto">
|
||||
<span *ngIf="!organization.enabled" class="tw-ml-auto">
|
||||
<i
|
||||
class="bwi bwi-fw bwi-exclamation-triangle text-danger mr-auto"
|
||||
attr.aria-label="{{ 'organizationIsDisabled' | i18n }}"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { newGuid } from "@bitwarden/guid";
|
||||
|
||||
import { getNestedCollectionTree, getFlatCollectionTree } from "./collection-utils";
|
||||
|
||||
@@ -9,11 +11,17 @@ describe("CollectionUtils Service", () => {
|
||||
// Arrange
|
||||
const collections: CollectionView[] = [];
|
||||
|
||||
const parentCollection = new CollectionView();
|
||||
parentCollection.name = "Parent";
|
||||
const parentCollection = new CollectionView({
|
||||
name: "Parent",
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
id: newGuid() as CollectionId,
|
||||
});
|
||||
|
||||
const childCollection = new CollectionView();
|
||||
childCollection.name = "Parent/Child";
|
||||
const childCollection = new CollectionView({
|
||||
name: "Parent/Child",
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
id: newGuid() as CollectionId,
|
||||
});
|
||||
|
||||
collections.push(childCollection);
|
||||
collections.push(parentCollection);
|
||||
@@ -41,12 +49,14 @@ describe("CollectionUtils Service", () => {
|
||||
describe("getFlatCollectionTree", () => {
|
||||
it("should flatten a tree node with no children", () => {
|
||||
// Arrange
|
||||
const collection = new CollectionView();
|
||||
collection.name = "Test Collection";
|
||||
collection.id = "test-id";
|
||||
const collection = new CollectionView({
|
||||
name: "Test Collection",
|
||||
id: "test-id" as CollectionId,
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
});
|
||||
|
||||
const treeNodes: TreeNode<CollectionView>[] = [
|
||||
new TreeNode<CollectionView>(collection, null),
|
||||
new TreeNode<CollectionView>(collection, {} as TreeNode<CollectionView>),
|
||||
];
|
||||
|
||||
// Act
|
||||
@@ -59,23 +69,34 @@ describe("CollectionUtils Service", () => {
|
||||
|
||||
it("should flatten a tree node with children", () => {
|
||||
// Arrange
|
||||
const parentCollection = new CollectionView();
|
||||
parentCollection.name = "Parent";
|
||||
parentCollection.id = "parent-id";
|
||||
const parentCollection = new CollectionView({
|
||||
name: "Parent",
|
||||
id: "parent-id" as CollectionId,
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
});
|
||||
|
||||
const child1Collection = new CollectionView();
|
||||
child1Collection.name = "Child 1";
|
||||
child1Collection.id = "child1-id";
|
||||
const child1Collection = new CollectionView({
|
||||
name: "Child 1",
|
||||
id: "child1-id" as CollectionId,
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
});
|
||||
|
||||
const child2Collection = new CollectionView();
|
||||
child2Collection.name = "Child 2";
|
||||
child2Collection.id = "child2-id";
|
||||
const child2Collection = new CollectionView({
|
||||
name: "Child 2",
|
||||
id: "child2-id" as CollectionId,
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
});
|
||||
|
||||
const grandchildCollection = new CollectionView();
|
||||
grandchildCollection.name = "Grandchild";
|
||||
grandchildCollection.id = "grandchild-id";
|
||||
const grandchildCollection = new CollectionView({
|
||||
name: "Grandchild",
|
||||
id: "grandchild-id" as CollectionId,
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
});
|
||||
|
||||
const parentNode = new TreeNode<CollectionView>(parentCollection, null);
|
||||
const parentNode = new TreeNode<CollectionView>(
|
||||
parentCollection,
|
||||
{} as TreeNode<CollectionView>,
|
||||
);
|
||||
const child1Node = new TreeNode<CollectionView>(child1Collection, parentNode);
|
||||
const child2Node = new TreeNode<CollectionView>(child2Collection, parentNode);
|
||||
const grandchildNode = new TreeNode<CollectionView>(grandchildCollection, child1Node);
|
||||
|
||||
@@ -22,7 +22,7 @@ export function getNestedCollectionTree(
|
||||
// Collections need to be cloned because ServiceUtils.nestedTraverse actively
|
||||
// modifies the names of collections.
|
||||
// These changes risk affecting collections store in StateService.
|
||||
const clonedCollections = collections
|
||||
const clonedCollections: CollectionView[] | CollectionAdminView[] = collections
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map(cloneCollection);
|
||||
|
||||
@@ -37,6 +37,21 @@ export function getNestedCollectionTree(
|
||||
return nodes;
|
||||
}
|
||||
|
||||
export function cloneCollection(collection: CollectionView): CollectionView;
|
||||
export function cloneCollection(collection: CollectionAdminView): CollectionAdminView;
|
||||
export function cloneCollection(
|
||||
collection: CollectionView | CollectionAdminView,
|
||||
): CollectionView | CollectionAdminView {
|
||||
let cloned;
|
||||
|
||||
if (collection instanceof CollectionAdminView) {
|
||||
cloned = Object.assign(new CollectionAdminView({ ...collection }), collection);
|
||||
} else {
|
||||
cloned = Object.assign(new CollectionView({ ...collection }), collection);
|
||||
}
|
||||
return cloned;
|
||||
}
|
||||
|
||||
export function getFlatCollectionTree(
|
||||
nodes: TreeNode<CollectionAdminView>[],
|
||||
): CollectionAdminView[];
|
||||
@@ -57,32 +72,3 @@ export function getFlatCollectionTree(
|
||||
return [node.node, ...children];
|
||||
});
|
||||
}
|
||||
|
||||
function cloneCollection(collection: CollectionView): CollectionView;
|
||||
function cloneCollection(collection: CollectionAdminView): CollectionAdminView;
|
||||
function cloneCollection(
|
||||
collection: CollectionView | CollectionAdminView,
|
||||
): CollectionView | CollectionAdminView {
|
||||
let cloned;
|
||||
|
||||
if (collection instanceof CollectionAdminView) {
|
||||
cloned = new CollectionAdminView();
|
||||
cloned.groups = [...collection.groups];
|
||||
cloned.users = [...collection.users];
|
||||
cloned.assigned = collection.assigned;
|
||||
cloned.unmanaged = collection.unmanaged;
|
||||
} else {
|
||||
cloned = new CollectionView();
|
||||
}
|
||||
|
||||
cloned.id = collection.id;
|
||||
cloned.externalId = collection.externalId;
|
||||
cloned.hidePasswords = collection.hidePasswords;
|
||||
cloned.name = collection.name;
|
||||
cloned.organizationId = collection.organizationId;
|
||||
cloned.readOnly = collection.readOnly;
|
||||
cloned.manage = collection.manage;
|
||||
cloned.type = collection.type;
|
||||
|
||||
return cloned;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { firstValueFrom, switchMap } from "rxjs";
|
||||
|
||||
import {
|
||||
CollectionAdminService,
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
@@ -99,6 +101,7 @@ export class VaultHeaderComponent {
|
||||
private dialogService: DialogService,
|
||||
private collectionAdminService: CollectionAdminService,
|
||||
private router: Router,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
get title() {
|
||||
@@ -199,7 +202,14 @@ export class VaultHeaderComponent {
|
||||
|
||||
async addCollection() {
|
||||
if (this.organization.productTierType === ProductTierType.Free) {
|
||||
const collections = await this.collectionAdminService.getAll(this.organization.id);
|
||||
const collections = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
this.collectionAdminService.collectionAdminViews$(this.organization.id, userId),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (collections.length === this.organization.maxCollections) {
|
||||
this.showFreeOrgUpgradeDialog();
|
||||
return;
|
||||
|
||||
@@ -79,8 +79,11 @@ import {
|
||||
DecryptionFailureDialogComponent,
|
||||
PasswordRepromptService,
|
||||
} from "@bitwarden/vault";
|
||||
import { OrganizationResellerRenewalWarningComponent } from "@bitwarden/web-vault/app/billing/warnings/components/organization-reseller-renewal-warning.component";
|
||||
import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/warnings/services/organization-warnings.service";
|
||||
import {
|
||||
OrganizationFreeTrialWarningComponent,
|
||||
OrganizationResellerRenewalWarningComponent,
|
||||
} from "@bitwarden/web-vault/app/billing/organizations/warnings/components";
|
||||
import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services";
|
||||
import { VaultItemsComponent } from "@bitwarden/web-vault/app/vault/components/vault-items/vault-items.component";
|
||||
|
||||
import { BillingNotificationService } from "../../../billing/services/billing-notification.service";
|
||||
@@ -90,7 +93,6 @@ import {
|
||||
} from "../../../billing/services/reseller-warning.service";
|
||||
import { TrialFlowService } from "../../../billing/services/trial-flow.service";
|
||||
import { FreeTrial } from "../../../billing/types/free-trial";
|
||||
import { OrganizationFreeTrialWarningComponent } from "../../../billing/warnings/components/organization-free-trial-warning.component";
|
||||
import { SharedModule } from "../../../shared";
|
||||
import { AssignCollectionsWebComponent } from "../../../vault/components/assign-collections";
|
||||
import {
|
||||
@@ -363,7 +365,12 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.allCollectionsWithoutUnassigned$ = this.refresh$.pipe(
|
||||
switchMap(() => organizationId$),
|
||||
switchMap((orgId) => this.collectionAdminService.getAll(orgId)),
|
||||
switchMap((orgId) =>
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.collectionAdminService.collectionAdminViews$(orgId, userId)),
|
||||
),
|
||||
),
|
||||
shareReplay({ refCount: false, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
@@ -386,11 +393,13 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
// FIXME: We should not assert that the Unassigned type is a CollectionId.
|
||||
// Instead we should consider representing the Unassigned collection as a different object, given that
|
||||
// it is not actually a collection.
|
||||
const noneCollection = new CollectionAdminView();
|
||||
noneCollection.name = this.i18nService.t("unassigned");
|
||||
noneCollection.id = Unassigned as CollectionId;
|
||||
noneCollection.organizationId = organizationId;
|
||||
return allCollections.concat(noneCollection);
|
||||
return allCollections.concat(
|
||||
new CollectionAdminView({
|
||||
name: this.i18nService.t("unassigned"),
|
||||
id: Unassigned as CollectionId,
|
||||
organizationId,
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -667,6 +676,15 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
organization$
|
||||
.pipe(
|
||||
switchMap((organization) =>
|
||||
this.organizationWarningsService.showSubscribeBeforeFreeTrialEndsDialog$(organization),
|
||||
),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
const freeTrial$ = combineLatest([
|
||||
organization$,
|
||||
this.hasSubscription$.pipe(filter((hasSubscription) => hasSubscription !== null)),
|
||||
|
||||
@@ -150,6 +150,12 @@
|
||||
>
|
||||
{{ "accessingUsingProvider" | i18n: organization.providerName }}
|
||||
</bit-banner>
|
||||
<app-tax-id-warning
|
||||
[subscriber]="subscriber$ | async"
|
||||
[getWarning$]="getTaxIdWarning$"
|
||||
(billingAddressUpdated)="refreshTaxIdWarning()"
|
||||
>
|
||||
</app-tax-id-warning>
|
||||
</ng-container>
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
@@ -28,6 +28,11 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { getById } from "@bitwarden/common/platform/misc";
|
||||
import { BannerModule, IconModule, AdminConsoleLogo } from "@bitwarden/components";
|
||||
import { OrganizationWarningsModule } from "@bitwarden/web-vault/app/billing/organizations/warnings/organization-warnings.module";
|
||||
import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services";
|
||||
import { NonIndividualSubscriber } from "@bitwarden/web-vault/app/billing/types";
|
||||
import { TaxIdWarningComponent } from "@bitwarden/web-vault/app/billing/warnings/components";
|
||||
import { TaxIdWarningType } from "@bitwarden/web-vault/app/billing/warnings/types";
|
||||
|
||||
import { FreeFamiliesPolicyService } from "../../../billing/services/free-families-policy.service";
|
||||
import { OrgSwitcherComponent } from "../../../layouts/org-switcher/org-switcher.component";
|
||||
@@ -44,6 +49,9 @@ import { WebLayoutModule } from "../../../layouts/web-layout.module";
|
||||
IconModule,
|
||||
OrgSwitcherComponent,
|
||||
BannerModule,
|
||||
TaxIdWarningComponent,
|
||||
TaxIdWarningComponent,
|
||||
OrganizationWarningsModule,
|
||||
],
|
||||
})
|
||||
export class OrganizationLayoutComponent implements OnInit {
|
||||
@@ -58,7 +66,6 @@ export class OrganizationLayoutComponent implements OnInit {
|
||||
showPaymentAndHistory$: Observable<boolean>;
|
||||
hideNewOrgButton$: Observable<boolean>;
|
||||
organizationIsUnmanaged$: Observable<boolean>;
|
||||
enterpriseOrganization$: Observable<boolean>;
|
||||
|
||||
protected isBreadcrumbEventLogsEnabled$: Observable<boolean>;
|
||||
protected showSponsoredFamiliesDropdown$: Observable<boolean>;
|
||||
@@ -69,6 +76,9 @@ export class OrganizationLayoutComponent implements OnInit {
|
||||
textKey: string;
|
||||
}>;
|
||||
|
||||
protected subscriber$: Observable<NonIndividualSubscriber>;
|
||||
protected getTaxIdWarning$: () => Observable<TaxIdWarningType | null>;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private organizationService: OrganizationService,
|
||||
@@ -79,6 +89,7 @@ export class OrganizationLayoutComponent implements OnInit {
|
||||
private accountService: AccountService,
|
||||
private freeFamiliesPolicyService: FreeFamiliesPolicyService,
|
||||
private organizationBillingService: OrganizationBillingServiceAbstraction,
|
||||
private organizationWarningsService: OrganizationWarningsService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -150,6 +161,20 @@ export class OrganizationLayoutComponent implements OnInit {
|
||||
: { route: "billing/payment-method", textKey: "paymentMethod" },
|
||||
),
|
||||
);
|
||||
|
||||
this.subscriber$ = this.organization$.pipe(
|
||||
map((organization) => ({
|
||||
type: "organization",
|
||||
data: organization,
|
||||
})),
|
||||
);
|
||||
|
||||
this.getTaxIdWarning$ = () =>
|
||||
this.organization$.pipe(
|
||||
switchMap((organization) =>
|
||||
this.organizationWarningsService.getTaxIdWarning$(organization),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
canShowVaultTab(organization: Organization): boolean {
|
||||
@@ -179,4 +204,6 @@ export class OrganizationLayoutComponent implements OnInit {
|
||||
getReportTabLabel(organization: Organization): string {
|
||||
return organization.useEvents ? "reporting" : "reports";
|
||||
}
|
||||
|
||||
refreshTaxIdWarning = () => this.organizationWarningsService.refreshTaxIdWarning();
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ import {
|
||||
openChangePlanDialog,
|
||||
} from "../../../billing/organizations/change-plan-dialog.component";
|
||||
import { EventService } from "../../../core";
|
||||
import { HeaderModule } from "../../../layouts/header/header.module";
|
||||
import { SharedModule } from "../../../shared";
|
||||
import { EventExportService } from "../../../tools/event-export";
|
||||
import { BaseEventsComponent } from "../../common/base.events.component";
|
||||
|
||||
@@ -46,9 +48,8 @@ const EVENT_SYSTEM_USER_TO_TRANSLATION: Record<EventSystemUser, string> = {
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: "app-org-events",
|
||||
templateUrl: "events.component.html",
|
||||
standalone: false,
|
||||
imports: [SharedModule, HeaderModule],
|
||||
})
|
||||
export class EventsComponent extends BaseEventsComponent implements OnInit, OnDestroy {
|
||||
exportFileName = "org-events";
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -156,7 +157,11 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
private orgCollections$ = from(this.collectionAdminService.getAll(this.organizationId)).pipe(
|
||||
private orgCollections$ = this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
this.collectionAdminService.collectionAdminViews$(this.organizationId, userId),
|
||||
),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
|
||||
@@ -253,8 +253,8 @@ export class GroupsComponent {
|
||||
private toCollectionMap(
|
||||
response: ListResponse<CollectionResponse>,
|
||||
): Observable<Record<string, CollectionView>> {
|
||||
const collections = response.data.map(
|
||||
(r) => new Collection(new CollectionData(r as CollectionDetailsResponse)),
|
||||
const collections = response.data.map((r) =>
|
||||
Collection.fromCollectionData(new CollectionData(r as CollectionDetailsResponse)),
|
||||
);
|
||||
|
||||
return this.accountService.activeAccount$.pipe(
|
||||
|
||||
@@ -8,6 +8,8 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { SharedModule } from "../../../shared";
|
||||
|
||||
export type UserConfirmDialogData = {
|
||||
name: string;
|
||||
userId: string;
|
||||
@@ -16,9 +18,8 @@ export type UserConfirmDialogData = {
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: "app-user-confirm",
|
||||
templateUrl: "user-confirm.component.html",
|
||||
standalone: false,
|
||||
imports: [SharedModule],
|
||||
})
|
||||
export class UserConfirmComponent implements OnInit {
|
||||
name: string;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user