1
0
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:
Nick Krantz
2025-08-19 11:18:57 -05:00
405 changed files with 7515 additions and 7184 deletions

1
.github/CODEOWNERS vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View 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 }}"
}

View File

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

View File

@@ -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é."

View File

@@ -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": "Επανεκκίνηση εγγραφής"

View File

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

View File

@@ -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": {

View File

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

View File

@@ -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": {

View File

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

View File

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

View File

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

View File

@@ -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",
});

View File

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

View File

@@ -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>();

View File

@@ -1866,7 +1866,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
frameId: this.focusedFieldData.frameId || 0,
},
);
}, 150);
}, 300);
}
/**

View File

@@ -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();

View File

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

View File

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

View 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 };

View File

@@ -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[];

View File

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

View File

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

View File

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

View File

@@ -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$,
);

View File

@@ -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");
});

View File

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

View File

@@ -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", () => {

View File

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

View File

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

View File

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

View File

@@ -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,
});
});
});
});
});

View File

@@ -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);
}
}
}
}
/**

View File

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

View File

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

View File

@@ -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" },
);
}

View File

@@ -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`,
});
},
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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();

View File

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

View File

@@ -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,
) {}
}

View File

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

View File

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

View File

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

View File

@@ -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() {

View File

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

View File

@@ -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() {

View File

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

View File

@@ -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() {

View File

@@ -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"]);
});

View File

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

View File

@@ -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);
}
}

View File

@@ -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()", () => {

View File

@@ -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 }),
);

View File

@@ -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.");
}

View File

@@ -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();
}

View File

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

View File

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

View File

@@ -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.");

View File

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

View File

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

View File

@@ -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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = [];

View File

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

View File

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

View File

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

View File

@@ -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);
}

View File

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

View File

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

View File

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

View File

@@ -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": {

View File

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

View File

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

View File

@@ -1,5 +1 @@
@import "../../../../libs/components/src/tw-theme.css";
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -49,7 +49,7 @@
<i class="bwi bwi-fw bwi-business" aria-hidden="true"></i>
&nbsp;{{ 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>
&nbsp;{{ 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 }}"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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();
}

View File

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

View File

@@ -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 }),
);

View File

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

View File

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