mirror of
https://github.com/bitwarden/browser
synced 2026-02-19 19:04:01 +00:00
Merge branch 'main' into PM-20040-all-tasks-complete-banner
This commit is contained in:
@@ -338,7 +338,7 @@
|
||||
"message": "Pokračovat na bitwarden.com?"
|
||||
},
|
||||
"bitwardenForBusiness": {
|
||||
"message": "Bitwarden pro byznys"
|
||||
"message": "Bitwarden pro podnikání"
|
||||
},
|
||||
"bitwardenAuthenticator": {
|
||||
"message": "Autentifikátor Bitwarden"
|
||||
@@ -398,7 +398,7 @@
|
||||
"message": "Název složky"
|
||||
},
|
||||
"folderHintText": {
|
||||
"message": "Vnořit složku přidáním názvu nadřazené složky následovaného znakem \"/\". Příklad: Sociální/Fóra"
|
||||
"message": "Složku vnoříte přidáním názvu nadřazené složky následovaného znakem \"/\". Příklad: Sociální/Fóra"
|
||||
},
|
||||
"noFoldersAdded": {
|
||||
"message": "Nebyly přidány žádné složky"
|
||||
@@ -407,7 +407,7 @@
|
||||
"message": "Vytvořte složky pro organizaci Vašich položek trezoru"
|
||||
},
|
||||
"deleteFolderPermanently": {
|
||||
"message": "Opravdu chcete trvale smazat tuto složku?"
|
||||
"message": "Opravdu chcete tuto složku trvale smazat?"
|
||||
},
|
||||
"deleteFolder": {
|
||||
"message": "Smazat složku"
|
||||
@@ -531,10 +531,10 @@
|
||||
"message": "Zahrnout číslice"
|
||||
},
|
||||
"minNumbers": {
|
||||
"message": "Minimální počet číslic"
|
||||
"message": "Minimálně číslic"
|
||||
},
|
||||
"minSpecial": {
|
||||
"message": "Minimální počet speciálních znaků"
|
||||
"message": "Minimálně speciálních znaků"
|
||||
},
|
||||
"avoidAmbiguous": {
|
||||
"message": "Nepoužívat zaměnitelné znaky",
|
||||
@@ -1016,7 +1016,7 @@
|
||||
"description": "This is the folder for uncategorized items"
|
||||
},
|
||||
"enableAddLoginNotification": {
|
||||
"message": "Ptát se na přidání přihlášení"
|
||||
"message": "Zeptat se na přidání přihlášení"
|
||||
},
|
||||
"vaultSaveOptionsTitle": {
|
||||
"message": "Uložit do voleb trezoru"
|
||||
@@ -1049,7 +1049,7 @@
|
||||
"message": "Klepněte na položky pro automatické vyplnění v zobrazení trezoru"
|
||||
},
|
||||
"clickToAutofill": {
|
||||
"message": "Klepněte na položky v návrhu automatického vyplňování pro vyplnění"
|
||||
"message": "Klepnout na položky v návrhu automatického vyplňování pro vyplnění"
|
||||
},
|
||||
"clearClipboard": {
|
||||
"message": "Vymazat schránku",
|
||||
@@ -1305,7 +1305,7 @@
|
||||
"message": "Sdílené"
|
||||
},
|
||||
"bitwardenForBusinessPageDesc": {
|
||||
"message": "Bitwarden pro bvyznys Vám umožňuje sdílet položky v trezoru s ostatními prostřednictvím organizace. Více informací naleznete na bitwarden.com."
|
||||
"message": "Bitwarden pro podnikání Vám umožňuje sdílet položky v trezoru s ostatními prostřednictvím organizace. Více informací naleznete na bitwarden.com."
|
||||
},
|
||||
"moveToOrganization": {
|
||||
"message": "Přesunout do organizace"
|
||||
@@ -1660,7 +1660,7 @@
|
||||
"description": "Overlay appearance select option for showing the field on click of the overlay icon"
|
||||
},
|
||||
"enableAutoFillOnPageLoadSectionTitle": {
|
||||
"message": "Automaticky vyplnit údaje při načtení stránky"
|
||||
"message": "Automatické vyplnění údajů při načtení stránky"
|
||||
},
|
||||
"enableAutoFillOnPageLoad": {
|
||||
"message": "Automaticky vyplnit údaje při načtení stránky"
|
||||
@@ -1678,7 +1678,7 @@
|
||||
"message": "Více informací o automatickém vyplňování"
|
||||
},
|
||||
"defaultAutoFillOnPageLoad": {
|
||||
"message": "Výchozí nastavení automatického vyplňování pro položky přihlášení"
|
||||
"message": "Výchozí nastavení autom. vyplňování"
|
||||
},
|
||||
"defaultAutoFillOnPageLoadDesc": {
|
||||
"message": "Můžete vypnout automatické vyplňování při načtení stránky pro jednotlivé přihlašovací položky v zobrazení pro úpravu položky."
|
||||
@@ -1690,10 +1690,10 @@
|
||||
"message": "Použít výchozí nastavení"
|
||||
},
|
||||
"autoFillOnPageLoadYes": {
|
||||
"message": "Automatické vyplnění při načtení stránky"
|
||||
"message": "Automatické vyplnění při načtení"
|
||||
},
|
||||
"autoFillOnPageLoadNo": {
|
||||
"message": "Nevyplňovat automaticky při načtení stránky"
|
||||
"message": "Nevyplňovat automaticky při načtení"
|
||||
},
|
||||
"commandOpenPopup": {
|
||||
"message": "Otevřít vyskakovací okno trezoru"
|
||||
@@ -1851,7 +1851,7 @@
|
||||
"message": "Slečna"
|
||||
},
|
||||
"dr": {
|
||||
"message": "MUDr."
|
||||
"message": "Dr."
|
||||
},
|
||||
"mx": {
|
||||
"message": "Neutrální"
|
||||
@@ -1875,7 +1875,7 @@
|
||||
"message": "Společnost"
|
||||
},
|
||||
"ssn": {
|
||||
"message": "Číslo sociálního pojištění"
|
||||
"message": "Rodné číslo"
|
||||
},
|
||||
"passportNumber": {
|
||||
"message": "Číslo cestovního pasu"
|
||||
@@ -2099,7 +2099,7 @@
|
||||
"message": "Nic k zobrazení"
|
||||
},
|
||||
"nothingGeneratedRecently": {
|
||||
"message": "Nedávno jste nic nevygenerovali"
|
||||
"message": "Ještě jste nic nevygenerovali"
|
||||
},
|
||||
"remove": {
|
||||
"message": "Odebrat"
|
||||
@@ -2233,7 +2233,7 @@
|
||||
"description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'"
|
||||
},
|
||||
"useGeneratorHelpTextPartTwo": {
|
||||
"message": "pro vytvoření silného jedinečného hesla",
|
||||
"message": "pro vytvoření silného jedinečného hesla.",
|
||||
"description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'"
|
||||
},
|
||||
"vaultCustomization": {
|
||||
@@ -2685,7 +2685,7 @@
|
||||
"message": "Změny v zablokovaných doménách byly uloženy"
|
||||
},
|
||||
"excludedDomainsSavedSuccess": {
|
||||
"message": "Vyloučené změny domény byly uloženy"
|
||||
"message": "Změny vyloučené domény byly uloženy"
|
||||
},
|
||||
"limitSendViews": {
|
||||
"message": "Omezit zobrazení"
|
||||
@@ -2911,7 +2911,7 @@
|
||||
"message": "Došlo k chybě při ukládání datumu smzání a vypršení platnosti."
|
||||
},
|
||||
"hideYourEmail": {
|
||||
"message": "Skryje Vaši e-mailovou adresu před zobrazením."
|
||||
"message": "Skrýt Vaši e-mailovou adresu"
|
||||
},
|
||||
"passwordPrompt": {
|
||||
"message": "Zeptat se znovu na hlavní heslo"
|
||||
@@ -3228,7 +3228,7 @@
|
||||
"description": "Labels the domain name email forwarder service option"
|
||||
},
|
||||
"forwarderDomainNameHint": {
|
||||
"message": "Vyberte doménu, která je podporována vybranou službou",
|
||||
"message": "Vyberte doménu, která je podporována vybranou službou.",
|
||||
"description": "Guidance provided for email forwarding services that support multiple email domains."
|
||||
},
|
||||
"forwarderError": {
|
||||
@@ -4132,7 +4132,7 @@
|
||||
"message": "Zvolte sbírku"
|
||||
},
|
||||
"importTargetHint": {
|
||||
"message": "Pokud chcete obsah importovaného souboru přesunout do složky $DESTINATION$, vyberte tuto volbu",
|
||||
"message": "Pokud chcete obsah importovaného souboru přesunout do: \"$DESTINATION$\", vyberte tuto volbu",
|
||||
"description": "Located as a hint under the import target. Will be appended by either folder or collection, depending if the user is importing into an individual or an organizational vault.",
|
||||
"placeholders": {
|
||||
"destination": {
|
||||
@@ -4407,7 +4407,7 @@
|
||||
"description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page"
|
||||
},
|
||||
"confirmContinueToHelpCenterPasswordManagementContent": {
|
||||
"message": "Změňte nastavení automatického vyplňování a správy hesel.",
|
||||
"message": "Změna nastavení automatického vyplňování a nastavení správy hesel.",
|
||||
"description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser password management settings"
|
||||
},
|
||||
"confirmContinueToHelpCenterKeyboardShortcutsContent": {
|
||||
@@ -4415,7 +4415,7 @@
|
||||
"description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser keyboard shortcut settings"
|
||||
},
|
||||
"confirmContinueToBrowserPasswordManagementSettingsContent": {
|
||||
"message": "Změňte nastavení automatického vyplňování a správy hesel.",
|
||||
"message": "Změna nastavení automatického vyplňování a nastavení správy hesel.",
|
||||
"description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page"
|
||||
},
|
||||
"confirmContinueToBrowserKeyboardShortcutSettingsContent": {
|
||||
@@ -4945,13 +4945,13 @@
|
||||
"message": "Popis pole"
|
||||
},
|
||||
"textHelpText": {
|
||||
"message": "Použijte textová pole pro data jako bezpečnostní otázky"
|
||||
"message": "Použijte textová pole pro data (jako např. bezpečnostní otázky)."
|
||||
},
|
||||
"hiddenHelpText": {
|
||||
"message": "Použijte skrytá pole pro citlivá data, jako je heslo"
|
||||
"message": "Použijte skrytá pole pro citlivá data, jako je heslo."
|
||||
},
|
||||
"checkBoxHelpText": {
|
||||
"message": "Použijte zaškrtávací políčka, pokud chcete automaticky vyplnit zaškrtávací políčko formuláře (např. pro zapamatování e-mailu)"
|
||||
"message": "Použijte zaškrtávací políčka, pokud chcete automaticky zvolit zaškrtávací políčko formuláře (např. pro zapamatování e-mailu)."
|
||||
},
|
||||
"linkedHelpText": {
|
||||
"message": "Použijte propojené pole, pokud máte problémy s automatickým vyplňováním na konkrétní webové stránce."
|
||||
@@ -5121,7 +5121,7 @@
|
||||
"message": "Zobrazit akce rychlé kopie v trezoru"
|
||||
},
|
||||
"systemDefault": {
|
||||
"message": "Systémový výchozí"
|
||||
"message": "Výchozí systémový"
|
||||
},
|
||||
"enterprisePolicyRequirementsApplied": {
|
||||
"message": "Na toto nastavení byly uplatněny požadavky podnikových zásad"
|
||||
@@ -5178,7 +5178,7 @@
|
||||
"message": "Položky, které smažete, se zde zobrazí a budou trvale smazány po 30 dnech."
|
||||
},
|
||||
"trashWarning": {
|
||||
"message": "Položky, které byly v koši déle než 30 dní, budou automaticky smazány."
|
||||
"message": "Položky, které byly v koši déle než 30 dnů, budou automaticky smazány."
|
||||
},
|
||||
"restore": {
|
||||
"message": "Obnovit"
|
||||
@@ -5394,10 +5394,10 @@
|
||||
"message": "Šířka rozšíření"
|
||||
},
|
||||
"wide": {
|
||||
"message": "Šířka"
|
||||
"message": "Široké"
|
||||
},
|
||||
"extraWide": {
|
||||
"message": "Extra široký"
|
||||
"message": "Extra široké"
|
||||
},
|
||||
"sshKeyWrongPassword": {
|
||||
"message": "Zadané heslo není správné."
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
"message": "Υπόδειξη κύριου κωδικού πρόσβασης (προαιρετικό)"
|
||||
},
|
||||
"passwordStrengthScore": {
|
||||
"message": "Password strength score $SCORE$",
|
||||
"message": "Βαθμολογία ισχύς κωδικού πρόσβασης $SCORE$",
|
||||
"placeholders": {
|
||||
"score": {
|
||||
"content": "$1",
|
||||
@@ -468,13 +468,13 @@
|
||||
"message": "Ο κωδικός δημιουργήθηκε"
|
||||
},
|
||||
"passphraseGenerated": {
|
||||
"message": "Passphrase generated"
|
||||
"message": "Το συνθηματικό δημιουργήθηκε"
|
||||
},
|
||||
"usernameGenerated": {
|
||||
"message": "Username generated"
|
||||
"message": "Το όνομα χρήστη δημιουργήθηκε"
|
||||
},
|
||||
"emailGenerated": {
|
||||
"message": "Email generated"
|
||||
"message": "Το email δημιουργήθηκε"
|
||||
},
|
||||
"regeneratePassword": {
|
||||
"message": "Επαναδημιουργία κωδικού πρόσβασης"
|
||||
@@ -548,7 +548,7 @@
|
||||
"message": "Αναζήτηση στο vault"
|
||||
},
|
||||
"resetSearch": {
|
||||
"message": "Reset search"
|
||||
"message": "Επαναφορά αναζήτησης"
|
||||
},
|
||||
"edit": {
|
||||
"message": "Επεξεργασία"
|
||||
@@ -659,10 +659,10 @@
|
||||
"message": "Το πρόγραμμα περιήγησης ιστού δεν υποστηρίζει εύκολη αντιγραφή πρόχειρου. Αντιγράψτε το με το χέρι αντ'αυτού."
|
||||
},
|
||||
"verifyYourIdentity": {
|
||||
"message": "Verify your identity"
|
||||
"message": "Επαλήθευση ταυτότητας"
|
||||
},
|
||||
"weDontRecognizeThisDevice": {
|
||||
"message": "We don't recognize this device. Enter the code sent to your email to verify your identity."
|
||||
"message": "Δεν αναγνωρίζουμε αυτή τη συσκευή. Εισάγετε τον κωδικό που στάλθηκε στο email σας για να επαληθεύσετε την ταυτότητά σας."
|
||||
},
|
||||
"continueLoggingIn": {
|
||||
"message": "Continue logging in"
|
||||
@@ -875,22 +875,22 @@
|
||||
"message": "Σύνδεση στο Bitwarden"
|
||||
},
|
||||
"enterTheCodeSentToYourEmail": {
|
||||
"message": "Enter the code sent to your email"
|
||||
"message": "Εισάγετε τον κωδικό που στάλθηκε στο email σας"
|
||||
},
|
||||
"enterTheCodeFromYourAuthenticatorApp": {
|
||||
"message": "Enter the code from your authenticator app"
|
||||
"message": "Εισαγάγετε τον κωδικό μιας χρήσης από την εφαρμογή αυθεντικοποίησης"
|
||||
},
|
||||
"pressYourYubiKeyToAuthenticate": {
|
||||
"message": "Press your YubiKey to authenticate"
|
||||
"message": "Πιέστε το YubiKey σας για ταυτοποίηση"
|
||||
},
|
||||
"duoTwoFactorRequiredPageSubtitle": {
|
||||
"message": "Duo two-step login is required for your account. Follow the steps below to finish logging in."
|
||||
"message": "Απαιτείται σύνδεση Duo δύο βημάτων για το λογαριασμό σας. Ακολουθήστε τα παρακάτω βήματα για να ολοκληρώσετε τη σύνδεση."
|
||||
},
|
||||
"followTheStepsBelowToFinishLoggingIn": {
|
||||
"message": "Follow the steps below to finish logging in."
|
||||
"message": "Ακολουθήστε τα παρακάτω βήματα για να ολοκληρώσετε τη σύνδεση."
|
||||
},
|
||||
"followTheStepsBelowToFinishLoggingInWithSecurityKey": {
|
||||
"message": "Follow the steps below to finish logging in with your security key."
|
||||
"message": "Ακολουθήστε τα παρακάτω βήματα για να ολοκληρώσετε τη σύνδεση με το κλειδί ασφαλείας σας."
|
||||
},
|
||||
"restartRegistration": {
|
||||
"message": "Επανεκκίνηση εγγραφής"
|
||||
|
||||
@@ -5575,9 +5575,9 @@
|
||||
"description": "'WebAssembly' is a technical term and should not be translated."
|
||||
},
|
||||
"showMore": {
|
||||
"message": "Show more"
|
||||
"message": "Prikaži više"
|
||||
},
|
||||
"showLess": {
|
||||
"message": "Show less"
|
||||
"message": "Pokaži manje"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -605,7 +605,7 @@
|
||||
"message": "Izdzēst vienumu"
|
||||
},
|
||||
"viewItem": {
|
||||
"message": "Skatīt vienumu"
|
||||
"message": "Apskatīt vienumu"
|
||||
},
|
||||
"launch": {
|
||||
"message": "Palaist"
|
||||
@@ -1180,7 +1180,7 @@
|
||||
"message": "Pēc savas paroles nomainīšanas būs nepieciešams pieteikties ar jauno paroli. Spēkā esošajās sesijās citās ierīcēs stundas laikā notiks atteikšanās."
|
||||
},
|
||||
"accountRecoveryUpdateMasterPasswordSubtitle": {
|
||||
"message": "Jānomaina sava galvenā'parole, lai pabeigtu konta atkopi."
|
||||
"message": "Jānomaina sava galvenā parole, lai pabeigtu konta atkopi."
|
||||
},
|
||||
"enableChangedPasswordNotification": {
|
||||
"message": "Vaicāt atjaunināt esošu pieteikšanās vienumu"
|
||||
@@ -1490,7 +1490,7 @@
|
||||
"description": "Select another two-step login method"
|
||||
},
|
||||
"useYourRecoveryCode": {
|
||||
"message": "Izmantot savu atkopes kodu"
|
||||
"message": "Izmanto savu atkopes kodu"
|
||||
},
|
||||
"insertU2f": {
|
||||
"message": "Ievieto savu drošības atslēgu datora USB ligzdā! Ja tai ir poga, pieskaries tai!"
|
||||
@@ -1523,10 +1523,10 @@
|
||||
"message": "Atlasīt divpakāpju pieteikšanās veidu"
|
||||
},
|
||||
"recoveryCodeDesc": {
|
||||
"message": "Zaudēta piekļuve visiem divpakāpju nodrošinātājiem? Izmanto atkopšanas kodus, lai atspējotu visus sava konta divpakāpju nodrošinātājus!"
|
||||
"message": "Zaudēta piekļuve visiem divpakāpju nodrošinātājiem? Izmanto atkopes kodus, lai atspējotu visus sava konta divpakāpju nodrošinātājus!"
|
||||
},
|
||||
"recoveryCodeTitle": {
|
||||
"message": "Atgūšanas kods"
|
||||
"message": "Atkopes kods"
|
||||
},
|
||||
"authenticatorAppTitle": {
|
||||
"message": "Autentificētāja lietotne"
|
||||
@@ -3437,7 +3437,7 @@
|
||||
"message": "Atkārtoti nosūtīt paziņojumu"
|
||||
},
|
||||
"viewAllLogInOptions": {
|
||||
"message": "Skatīt visas pieteikšanās iespējas"
|
||||
"message": "Apskatīt visas pieteikšanās iespējas"
|
||||
},
|
||||
"notificationSentDevice": {
|
||||
"message": "Uz ierīci ir nosūtīts paziņojums."
|
||||
@@ -3728,7 +3728,7 @@
|
||||
"description": "European Union"
|
||||
},
|
||||
"accessDenied": {
|
||||
"message": "Piekļuve liegta. Nav nepieciešamo atļauju, lai skatītu šo lapu."
|
||||
"message": "Piekļuve liegta. Nav nepieciešamo atļauju, lai apskatītu šo lapu."
|
||||
},
|
||||
"general": {
|
||||
"message": "Vispārīgi"
|
||||
@@ -4387,7 +4387,7 @@
|
||||
"description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy"
|
||||
},
|
||||
"startsWithAdvancedOptionWarning": {
|
||||
"message": "\"Sākas ar' ir lietpratējiem paredzēta iespēja ar paaugstinātu piekļuves datu atklāšanas bīstamību.",
|
||||
"message": "“Sākas ar” ir lietpratējiem paredzēta iespēja ar paaugstinātu piekļuves datu atklāšanas bīstamību.",
|
||||
"description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy"
|
||||
},
|
||||
"uriMatchWarningDialogLink": {
|
||||
@@ -4411,7 +4411,7 @@
|
||||
"description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser password management settings"
|
||||
},
|
||||
"confirmContinueToHelpCenterKeyboardShortcutsContent": {
|
||||
"message": "Paplašinājuma īsinājumtaustiņus skatīt un iestatīt var pārlūka iestatījumos.",
|
||||
"message": "Paplašinājuma īsinājumtaustiņus apskatīt un iestatīt var pārlūka iestatījumos.",
|
||||
"description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser keyboard shortcut settings"
|
||||
},
|
||||
"confirmContinueToBrowserPasswordManagementSettingsContent": {
|
||||
@@ -4419,7 +4419,7 @@
|
||||
"description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page"
|
||||
},
|
||||
"confirmContinueToBrowserKeyboardShortcutSettingsContent": {
|
||||
"message": "Paplašinājuma īsinājumtaustiņus skatīt un iestatīt var pārlūka iestatījumos.",
|
||||
"message": "Paplašinājuma īsinājumtaustiņus apskatīt un iestatīt var pārlūka iestatījumos.",
|
||||
"description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page"
|
||||
},
|
||||
"overrideDefaultBrowserAutofillTitle": {
|
||||
@@ -4534,7 +4534,7 @@
|
||||
}
|
||||
},
|
||||
"viewItemTitle": {
|
||||
"message": "Skatīt vienumu - $ITEMNAME$",
|
||||
"message": "Apskatīt vienumu - $ITEMNAME$",
|
||||
"description": "Title for a link that opens a view for an item.",
|
||||
"placeholders": {
|
||||
"itemname": {
|
||||
@@ -4629,7 +4629,7 @@
|
||||
"message": "Kļūda mērķa mapes piešķiršanā."
|
||||
},
|
||||
"viewItemsIn": {
|
||||
"message": "Skatīt $NAME$ vienumus",
|
||||
"message": "Apskatīt $NAME$ vienumus",
|
||||
"description": "Button to view the contents of a folder or collection",
|
||||
"placeholders": {
|
||||
"name": {
|
||||
|
||||
@@ -1153,7 +1153,7 @@
|
||||
"description": "Shown to user after login is updated."
|
||||
},
|
||||
"loginUpdateTaskSuccessAdditional": {
|
||||
"message": "Dziękujemy za dbanie o bezpieczeństwo $ORGANIZATION$. Pozostało $TASK_COUNT$ haseł do zaktualizowania.",
|
||||
"message": "Dziękujemy za zwiększenie bezpieczeństwa organizacji $ORGANIZATION$. Zaktualizuj hasła dla jeszcze $TASK_COUNT$ danych logowania.",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1"
|
||||
@@ -1773,7 +1773,7 @@
|
||||
"message": "Pokaż licznik na ikonie"
|
||||
},
|
||||
"badgeCounterDesc": {
|
||||
"message": "Wskaż, ile masz danych logowania do bieżącej strony internetowej."
|
||||
"message": "Pokazuje liczbę danych logowania dla obecnej strony internetowej."
|
||||
},
|
||||
"cardholderName": {
|
||||
"message": "Właściciel karty"
|
||||
@@ -2482,7 +2482,7 @@
|
||||
"message": "Uprawnienie nie zostało przyznane"
|
||||
},
|
||||
"nativeMessaginPermissionErrorDesc": {
|
||||
"message": "Bez uprawnienia do komunikowania się z aplikacją desktopową Bitwarden nie możemy dostarczyć obsługi danych biometrycznych w rozszerzeniu przeglądarki. Spróbuj ponownie."
|
||||
"message": "Odblokowanie biometrią jest dostępne dopiero po połączeniu rozszerzenia przeglądarki z aplikacją desktopową Bitwarden. Spróbuj ponownie."
|
||||
},
|
||||
"nativeMessaginPermissionSidebarTitle": {
|
||||
"message": "Wystąpił błąd żądania uprawnienia"
|
||||
@@ -2610,7 +2610,7 @@
|
||||
"message": "Sprawdź i zmień 1 zagrożone hasło"
|
||||
},
|
||||
"reviewAndChangeAtRiskPasswordsPlural": {
|
||||
"message": "Przejrzyj i zmień $COUNT$ zagrożonych haseł ",
|
||||
"message": "Sprawdź i zmień zagrożone hasła ($COUNT$)",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
@@ -3380,7 +3380,7 @@
|
||||
"message": "Nie można uzyskać dostępu do elementów w zawieszonych organizacjach. Skontaktuj się z właścicielem organizacji, aby uzyskać pomoc."
|
||||
},
|
||||
"loggingInTo": {
|
||||
"message": "Logowanie na $DOMAIN$",
|
||||
"message": "Serwer: $DOMAIN$",
|
||||
"placeholders": {
|
||||
"domain": {
|
||||
"content": "$1",
|
||||
@@ -4354,7 +4354,7 @@
|
||||
"message": "serwer"
|
||||
},
|
||||
"hostedAt": {
|
||||
"message": "hostowany w"
|
||||
"message": "serwer"
|
||||
},
|
||||
"useDeviceOrHardwareKey": {
|
||||
"message": "Użyj urządzenia lub klucza sprzętowego"
|
||||
@@ -5025,7 +5025,7 @@
|
||||
"message": "Element zostanie przeniesiony do organizacji. Nie będziesz już właścicielem elementu."
|
||||
},
|
||||
"personalItemsTransferWarningPlural": {
|
||||
"message": "$PERSONAL_ITEMS_COUNT$ elementów zostanie trwale przeniesionych do wybranej organizacji. Nie będziesz już posiadać tych elementów.",
|
||||
"message": "Nie będziesz już właścicielem $PERSONAL_ITEMS_COUNT$ elementów przeniesionych do organizacji.",
|
||||
"placeholders": {
|
||||
"personal_items_count": {
|
||||
"content": "$1",
|
||||
@@ -5043,7 +5043,7 @@
|
||||
}
|
||||
},
|
||||
"personalItemsWithOrgTransferWarningPlural": {
|
||||
"message": "$PERSONAL_ITEMS_COUNT$ elementy zostaną trwale przeniesione do $ORG$. Nie będziesz już posiadać tych elementów.",
|
||||
"message": "Nie będziesz już właścicielem $PERSONAL_ITEMS_COUNT$ elementów przeniesionych do organizacji $ORG$.",
|
||||
"placeholders": {
|
||||
"personal_items_count": {
|
||||
"content": "$1",
|
||||
|
||||
@@ -4383,7 +4383,7 @@
|
||||
"description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item."
|
||||
},
|
||||
"regExAdvancedOptionWarning": {
|
||||
"message": "A \"expressão regular\" é uma opção avançada com um risco acrescido de exposição de credenciais.",
|
||||
"message": "A \"Expressão regular\" é uma opção avançada com um risco acrescido de exposição de credenciais.",
|
||||
"description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy"
|
||||
},
|
||||
"startsWithAdvancedOptionWarning": {
|
||||
|
||||
@@ -1836,7 +1836,7 @@
|
||||
"message": "số thẻ"
|
||||
},
|
||||
"ex": {
|
||||
"message": "Ví dụ:"
|
||||
"message": "ví dụ."
|
||||
},
|
||||
"title": {
|
||||
"message": "Tiêu đề"
|
||||
@@ -2458,7 +2458,7 @@
|
||||
"message": "Nhận dạng sinh trắc học không được hỗ trợ"
|
||||
},
|
||||
"biometricsNotSupportedDesc": {
|
||||
"message": "Nhận dạng sinh trắc học trên trình duyệt không được hỗ trợ trên thiết bị này"
|
||||
"message": "Nhận dạng sinh trắc học trên trình duyệt không được hỗ trợ trên thiết bị này."
|
||||
},
|
||||
"biometricsNotUnlockedTitle": {
|
||||
"message": "Người dùng đã khoá hoặc đã đăng xuất"
|
||||
|
||||
@@ -4,11 +4,29 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
import { CollectionView } from "../../content/components/common-types";
|
||||
import { NotificationQueueMessageTypes } from "../../enums/notification-queue-message-type.enum";
|
||||
import { NotificationType, NotificationTypes } from "../../enums/notification-type.enum";
|
||||
import AutofillPageDetails from "../../models/autofill-page-details";
|
||||
|
||||
/**
|
||||
* @todo Remove Standard_ label when implemented as standard NotificationQueueMessage.
|
||||
*/
|
||||
export interface Standard_NotificationQueueMessage<T, D> {
|
||||
// universal notification properties
|
||||
domain: string;
|
||||
tab: chrome.tabs.Tab;
|
||||
launchTimestamp: number;
|
||||
expires: Date;
|
||||
wasVaultLocked: boolean;
|
||||
|
||||
type: T; // NotificationType
|
||||
data: D; // notification-specific data
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Deprecate in favor of Standard_NotificationQueueMessage.
|
||||
*/
|
||||
interface NotificationQueueMessage {
|
||||
type: NotificationQueueMessageTypes;
|
||||
type: NotificationTypes;
|
||||
domain: string;
|
||||
tab: chrome.tabs.Tab;
|
||||
launchTimestamp: number;
|
||||
@@ -16,11 +34,15 @@ interface NotificationQueueMessage {
|
||||
wasVaultLocked: boolean;
|
||||
}
|
||||
|
||||
interface AddChangePasswordQueueMessage extends NotificationQueueMessage {
|
||||
type: "change";
|
||||
type ChangePasswordNotificationData = {
|
||||
cipherId: CipherView["id"];
|
||||
newPassword: string;
|
||||
}
|
||||
};
|
||||
|
||||
type AddChangePasswordNotificationQueueMessage = Standard_NotificationQueueMessage<
|
||||
typeof NotificationType.ChangePassword,
|
||||
ChangePasswordNotificationData
|
||||
>;
|
||||
|
||||
interface AddLoginQueueMessage extends NotificationQueueMessage {
|
||||
type: "add";
|
||||
@@ -41,7 +63,7 @@ interface AtRiskPasswordQueueMessage extends NotificationQueueMessage {
|
||||
|
||||
type NotificationQueueMessageItem =
|
||||
| AddLoginQueueMessage
|
||||
| AddChangePasswordQueueMessage
|
||||
| AddChangePasswordNotificationQueueMessage
|
||||
| AddUnlockVaultQueueMessage
|
||||
| AtRiskPasswordQueueMessage;
|
||||
|
||||
@@ -72,6 +94,11 @@ type UnlockVaultMessageData = {
|
||||
skipNotification?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* @todo Extend generics to this type, see Standard_NotificationQueueMessage
|
||||
* - use new `data` types as generic
|
||||
* - eliminate optional status of properties as needed per Notification Type
|
||||
*/
|
||||
type NotificationBackgroundExtensionMessage = {
|
||||
[key: string]: any;
|
||||
command: string;
|
||||
@@ -126,7 +153,7 @@ type NotificationBackgroundExtensionMessageHandlers = {
|
||||
};
|
||||
|
||||
export {
|
||||
AddChangePasswordQueueMessage,
|
||||
AddChangePasswordNotificationQueueMessage,
|
||||
AddLoginQueueMessage,
|
||||
AddUnlockVaultQueueMessage,
|
||||
NotificationQueueMessageItem,
|
||||
|
||||
@@ -26,14 +26,14 @@ import { FolderService } from "@bitwarden/common/vault/services/folder/folder.se
|
||||
import { TaskService, SecurityTask } from "@bitwarden/common/vault/tasks";
|
||||
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
import { NotificationQueueMessageType } from "../enums/notification-queue-message-type.enum";
|
||||
import { NotificationType } from "../enums/notification-type.enum";
|
||||
import { FormData } from "../services/abstractions/autofill.service";
|
||||
import AutofillService from "../services/autofill.service";
|
||||
import { createAutofillPageDetailsMock, createChromeTabMock } from "../spec/autofill-mocks";
|
||||
import { flushPromises, sendMockExtensionMessage } from "../spec/testing-utils";
|
||||
|
||||
import {
|
||||
AddChangePasswordQueueMessage,
|
||||
AddChangePasswordNotificationQueueMessage,
|
||||
AddLoginQueueMessage,
|
||||
AddUnlockVaultQueueMessage,
|
||||
LockedVaultPendingNotificationsData,
|
||||
@@ -761,7 +761,7 @@ describe("NotificationBackground", () => {
|
||||
notificationBackground["notificationQueue"] = [
|
||||
mock<AddUnlockVaultQueueMessage>({
|
||||
tab,
|
||||
type: NotificationQueueMessageType.UnlockVault,
|
||||
type: NotificationType.UnlockVault,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -783,7 +783,7 @@ describe("NotificationBackground", () => {
|
||||
};
|
||||
notificationBackground["notificationQueue"] = [
|
||||
mock<AddLoginQueueMessage>({
|
||||
type: NotificationQueueMessageType.AddLogin,
|
||||
type: NotificationType.AddLogin,
|
||||
tab,
|
||||
domain: "another.com",
|
||||
}),
|
||||
@@ -803,11 +803,11 @@ describe("NotificationBackground", () => {
|
||||
edit: false,
|
||||
folder: "folder-id",
|
||||
};
|
||||
const queueMessage = mock<AddChangePasswordQueueMessage>({
|
||||
type: NotificationQueueMessageType.ChangePassword,
|
||||
const queueMessage = mock<AddChangePasswordNotificationQueueMessage>({
|
||||
type: NotificationType.ChangePassword,
|
||||
tab,
|
||||
domain: "example.com",
|
||||
newPassword: "newPassword",
|
||||
data: { newPassword: "newPassword" },
|
||||
});
|
||||
notificationBackground["notificationQueue"] = [queueMessage];
|
||||
const cipherView = mock<CipherView>({
|
||||
@@ -825,7 +825,7 @@ describe("NotificationBackground", () => {
|
||||
expect(createWithServerSpy).not.toHaveBeenCalled();
|
||||
expect(updatePasswordSpy).toHaveBeenCalledWith(
|
||||
cipherView,
|
||||
queueMessage.newPassword,
|
||||
queueMessage.data.newPassword,
|
||||
message.edit,
|
||||
sender.tab,
|
||||
"testId",
|
||||
@@ -851,11 +851,11 @@ describe("NotificationBackground", () => {
|
||||
edit: false,
|
||||
folder: "folder-id",
|
||||
};
|
||||
const queueMessage = mock<AddChangePasswordQueueMessage>({
|
||||
type: NotificationQueueMessageType.ChangePassword,
|
||||
const queueMessage = mock<AddChangePasswordNotificationQueueMessage>({
|
||||
type: NotificationType.ChangePassword,
|
||||
tab,
|
||||
domain: "example.com",
|
||||
newPassword: "newPassword",
|
||||
data: { newPassword: "newPassword" },
|
||||
});
|
||||
notificationBackground["notificationQueue"] = [queueMessage];
|
||||
const cipherView = mock<CipherView>({
|
||||
@@ -874,7 +874,7 @@ describe("NotificationBackground", () => {
|
||||
expect(createWithServerSpy).not.toHaveBeenCalled();
|
||||
expect(updatePasswordSpy).toHaveBeenCalledWith(
|
||||
cipherView,
|
||||
queueMessage.newPassword,
|
||||
queueMessage.data.newPassword,
|
||||
message.edit,
|
||||
sender.tab,
|
||||
"testId",
|
||||
@@ -931,11 +931,11 @@ describe("NotificationBackground", () => {
|
||||
edit: false,
|
||||
folder: "folder-id",
|
||||
};
|
||||
const queueMessage = mock<AddChangePasswordQueueMessage>({
|
||||
type: NotificationQueueMessageType.ChangePassword,
|
||||
const queueMessage = mock<AddChangePasswordNotificationQueueMessage>({
|
||||
type: NotificationType.ChangePassword,
|
||||
tab,
|
||||
domain: "example.com",
|
||||
newPassword: "newPassword",
|
||||
data: { newPassword: "newPassword" },
|
||||
});
|
||||
notificationBackground["notificationQueue"] = [queueMessage];
|
||||
const cipherView = mock<CipherView>({
|
||||
@@ -953,7 +953,7 @@ describe("NotificationBackground", () => {
|
||||
expect(createWithServerSpy).not.toHaveBeenCalled();
|
||||
expect(updatePasswordSpy).toHaveBeenCalledWith(
|
||||
cipherView,
|
||||
queueMessage.newPassword,
|
||||
queueMessage.data.newPassword,
|
||||
message.edit,
|
||||
sender.tab,
|
||||
mockCipherId,
|
||||
@@ -983,7 +983,7 @@ describe("NotificationBackground", () => {
|
||||
folder: "folder-id",
|
||||
};
|
||||
const queueMessage = mock<AddLoginQueueMessage>({
|
||||
type: NotificationQueueMessageType.AddLogin,
|
||||
type: NotificationType.AddLogin,
|
||||
tab,
|
||||
domain: "example.com",
|
||||
username: "test",
|
||||
@@ -1018,11 +1018,11 @@ describe("NotificationBackground", () => {
|
||||
edit: true,
|
||||
folder: "folder-id",
|
||||
};
|
||||
const queueMessage = mock<AddChangePasswordQueueMessage>({
|
||||
type: NotificationQueueMessageType.ChangePassword,
|
||||
const queueMessage = mock<AddChangePasswordNotificationQueueMessage>({
|
||||
type: NotificationType.ChangePassword,
|
||||
tab,
|
||||
domain: "example.com",
|
||||
newPassword: "newPassword",
|
||||
data: { newPassword: "newPassword" },
|
||||
});
|
||||
notificationBackground["notificationQueue"] = [queueMessage];
|
||||
const cipherView = mock<CipherView>();
|
||||
@@ -1035,7 +1035,7 @@ describe("NotificationBackground", () => {
|
||||
|
||||
expect(updatePasswordSpy).toHaveBeenCalledWith(
|
||||
cipherView,
|
||||
queueMessage.newPassword,
|
||||
queueMessage.data.newPassword,
|
||||
message.edit,
|
||||
sender.tab,
|
||||
"testId",
|
||||
@@ -1070,7 +1070,7 @@ describe("NotificationBackground", () => {
|
||||
folder: "folder-id",
|
||||
};
|
||||
const queueMessage = mock<AddLoginQueueMessage>({
|
||||
type: NotificationQueueMessageType.AddLogin,
|
||||
type: NotificationType.AddLogin,
|
||||
tab,
|
||||
domain: "example.com",
|
||||
username: "test",
|
||||
@@ -1109,7 +1109,7 @@ describe("NotificationBackground", () => {
|
||||
folder: "folder-id",
|
||||
};
|
||||
const queueMessage = mock<AddLoginQueueMessage>({
|
||||
type: NotificationQueueMessageType.AddLogin,
|
||||
type: NotificationType.AddLogin,
|
||||
tab,
|
||||
domain: "example.com",
|
||||
username: "test",
|
||||
@@ -1162,7 +1162,7 @@ describe("NotificationBackground", () => {
|
||||
folder: "folder-id",
|
||||
};
|
||||
const queueMessage = mock<AddLoginQueueMessage>({
|
||||
type: NotificationQueueMessageType.AddLogin,
|
||||
type: NotificationType.AddLogin,
|
||||
tab,
|
||||
domain: "example.com",
|
||||
username: "test",
|
||||
@@ -1213,11 +1213,11 @@ describe("NotificationBackground", () => {
|
||||
edit: false,
|
||||
folder: "folder-id",
|
||||
};
|
||||
const queueMessage = mock<AddChangePasswordQueueMessage>({
|
||||
type: NotificationQueueMessageType.ChangePassword,
|
||||
const queueMessage = mock<AddChangePasswordNotificationQueueMessage>({
|
||||
type: NotificationType.ChangePassword,
|
||||
tab,
|
||||
domain: "example.com",
|
||||
newPassword: "newPassword",
|
||||
data: { newPassword: "newPassword" },
|
||||
});
|
||||
notificationBackground["notificationQueue"] = [queueMessage];
|
||||
const cipherView = mock<CipherView>({ reprompt: CipherRepromptType.None });
|
||||
@@ -1273,7 +1273,7 @@ describe("NotificationBackground", () => {
|
||||
const sender = mock<chrome.runtime.MessageSender>({ tab });
|
||||
const message: NotificationBackgroundExtensionMessage = { command: "bgNeverSave" };
|
||||
notificationBackground["notificationQueue"] = [
|
||||
mock<AddUnlockVaultQueueMessage>({ type: NotificationQueueMessageType.UnlockVault, tab }),
|
||||
mock<AddUnlockVaultQueueMessage>({ type: NotificationType.UnlockVault, tab }),
|
||||
];
|
||||
|
||||
sendMockExtensionMessage(message, sender);
|
||||
@@ -1289,7 +1289,7 @@ describe("NotificationBackground", () => {
|
||||
const sender = mock<chrome.runtime.MessageSender>({ tab: secondaryTab });
|
||||
notificationBackground["notificationQueue"] = [
|
||||
mock<AddLoginQueueMessage>({
|
||||
type: NotificationQueueMessageType.AddLogin,
|
||||
type: NotificationType.AddLogin,
|
||||
tab,
|
||||
domain: "another.com",
|
||||
}),
|
||||
@@ -1306,12 +1306,12 @@ describe("NotificationBackground", () => {
|
||||
const sender = mock<chrome.runtime.MessageSender>({ tab });
|
||||
const message: NotificationBackgroundExtensionMessage = { command: "bgNeverSave" };
|
||||
const firstNotification = mock<AddLoginQueueMessage>({
|
||||
type: NotificationQueueMessageType.AddLogin,
|
||||
type: NotificationType.AddLogin,
|
||||
tab,
|
||||
domain: "example.com",
|
||||
});
|
||||
const secondNotification = mock<AddLoginQueueMessage>({
|
||||
type: NotificationQueueMessageType.AddLogin,
|
||||
type: NotificationType.AddLogin,
|
||||
tab: createChromeTabMock({ id: 3 }),
|
||||
domain: "another.com",
|
||||
});
|
||||
|
||||
@@ -60,12 +60,12 @@ import {
|
||||
NotificationCipherData,
|
||||
} from "../content/components/cipher/types";
|
||||
import { CollectionView } from "../content/components/common-types";
|
||||
import { NotificationQueueMessageType } from "../enums/notification-queue-message-type.enum";
|
||||
import { NotificationType } from "../enums/notification-type.enum";
|
||||
import { AutofillService } from "../services/abstractions/autofill.service";
|
||||
import { TemporaryNotificationChangeLoginService } from "../services/notification-change-login-password.service";
|
||||
|
||||
import {
|
||||
AddChangePasswordQueueMessage,
|
||||
AddChangePasswordNotificationQueueMessage,
|
||||
AddLoginQueueMessage,
|
||||
AddUnlockVaultQueueMessage,
|
||||
AddLoginMessageData,
|
||||
@@ -208,16 +208,21 @@ export default class NotificationBackground {
|
||||
organizations.find((org) => org.id === orgId)?.productTierType;
|
||||
|
||||
const cipherQueueMessage = this.notificationQueue.find(
|
||||
(message): message is AddChangePasswordQueueMessage | AddLoginQueueMessage =>
|
||||
message.type === NotificationQueueMessageType.ChangePassword ||
|
||||
message.type === NotificationQueueMessageType.AddLogin,
|
||||
(message): message is AddChangePasswordNotificationQueueMessage | AddLoginQueueMessage =>
|
||||
message.type === NotificationType.ChangePassword ||
|
||||
message.type === NotificationType.AddLogin,
|
||||
);
|
||||
|
||||
if (cipherQueueMessage) {
|
||||
const cipherView =
|
||||
cipherQueueMessage.type === NotificationQueueMessageType.ChangePassword
|
||||
? await this.getDecryptedCipherById(cipherQueueMessage.cipherId, activeUserId)
|
||||
: this.convertAddLoginQueueMessageToCipherView(cipherQueueMessage);
|
||||
let cipherView: CipherView;
|
||||
if (cipherQueueMessage.type === NotificationType.ChangePassword) {
|
||||
const {
|
||||
data: { cipherId },
|
||||
} = cipherQueueMessage;
|
||||
cipherView = await this.getDecryptedCipherById(cipherId, activeUserId);
|
||||
} else {
|
||||
cipherView = this.convertAddLoginQueueMessageToCipherView(cipherQueueMessage);
|
||||
}
|
||||
|
||||
const organizationType = getOrganizationType(cipherView.organizationId);
|
||||
return [
|
||||
@@ -424,7 +429,7 @@ export default class NotificationBackground {
|
||||
};
|
||||
|
||||
switch (notificationType) {
|
||||
case NotificationQueueMessageType.AddLogin:
|
||||
case NotificationType.AddLogin:
|
||||
typeData.removeIndividualVault = await this.removeIndividualVault();
|
||||
break;
|
||||
}
|
||||
@@ -501,7 +506,7 @@ export default class NotificationBackground {
|
||||
const queueMessage: NotificationQueueMessageItem = {
|
||||
domain,
|
||||
wasVaultLocked,
|
||||
type: NotificationQueueMessageType.AtRiskPassword,
|
||||
type: NotificationType.AtRiskPassword,
|
||||
passwordChangeUri,
|
||||
organizationName: organization.name,
|
||||
tab: tab,
|
||||
@@ -591,7 +596,7 @@ export default class NotificationBackground {
|
||||
this.removeTabFromNotificationQueue(tab);
|
||||
const launchTimestamp = new Date().getTime();
|
||||
const message: AddLoginQueueMessage = {
|
||||
type: NotificationQueueMessageType.AddLogin,
|
||||
type: NotificationType.AddLogin,
|
||||
username: loginInfo.username,
|
||||
password: loginInfo.password,
|
||||
domain: loginDomain,
|
||||
@@ -716,10 +721,9 @@ export default class NotificationBackground {
|
||||
// remove any old messages for this tab
|
||||
this.removeTabFromNotificationQueue(tab);
|
||||
const launchTimestamp = new Date().getTime();
|
||||
const message: AddChangePasswordQueueMessage = {
|
||||
type: NotificationQueueMessageType.ChangePassword,
|
||||
cipherId: cipherId,
|
||||
newPassword: newPassword,
|
||||
const message: AddChangePasswordNotificationQueueMessage = {
|
||||
type: NotificationType.ChangePassword,
|
||||
data: { cipherId: cipherId, newPassword: newPassword },
|
||||
domain: loginDomain,
|
||||
tab: tab,
|
||||
launchTimestamp,
|
||||
@@ -734,7 +738,7 @@ export default class NotificationBackground {
|
||||
this.removeTabFromNotificationQueue(tab);
|
||||
const launchTimestamp = new Date().getTime();
|
||||
const message: AddUnlockVaultQueueMessage = {
|
||||
type: NotificationQueueMessageType.UnlockVault,
|
||||
type: NotificationType.UnlockVault,
|
||||
domain: loginDomain,
|
||||
tab: tab,
|
||||
launchTimestamp,
|
||||
@@ -804,8 +808,8 @@ export default class NotificationBackground {
|
||||
const queueMessage = this.notificationQueue[i];
|
||||
if (
|
||||
queueMessage.tab.id !== tab.id ||
|
||||
(queueMessage.type !== NotificationQueueMessageType.AddLogin &&
|
||||
queueMessage.type !== NotificationQueueMessageType.ChangePassword)
|
||||
(queueMessage.type !== NotificationType.AddLogin &&
|
||||
queueMessage.type !== NotificationType.ChangePassword)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
@@ -818,17 +822,13 @@ export default class NotificationBackground {
|
||||
this.accountService.activeAccount$.pipe(getOptionalUserId),
|
||||
);
|
||||
|
||||
if (queueMessage.type === NotificationQueueMessageType.ChangePassword) {
|
||||
const cipherView = await this.getDecryptedCipherById(queueMessage.cipherId, activeUserId);
|
||||
if (queueMessage.type === NotificationType.ChangePassword) {
|
||||
const {
|
||||
data: { cipherId, newPassword },
|
||||
} = queueMessage;
|
||||
const cipherView = await this.getDecryptedCipherById(cipherId, activeUserId);
|
||||
|
||||
await this.updatePassword(
|
||||
cipherView,
|
||||
queueMessage.newPassword,
|
||||
edit,
|
||||
tab,
|
||||
activeUserId,
|
||||
skipReprompt,
|
||||
);
|
||||
await this.updatePassword(cipherView, newPassword, edit, tab, activeUserId, skipReprompt);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -993,7 +993,7 @@ export default class NotificationBackground {
|
||||
|
||||
const queueItem = this.notificationQueue.find((item) => item.tab.id === senderTab.id);
|
||||
|
||||
if (queueItem?.type === NotificationQueueMessageType.AddLogin) {
|
||||
if (queueItem?.type === NotificationType.AddLogin) {
|
||||
const cipherView = this.convertAddLoginQueueMessageToCipherView(queueItem);
|
||||
cipherView.organizationId = organizationId;
|
||||
cipherView.folderId = folder;
|
||||
@@ -1075,10 +1075,7 @@ export default class NotificationBackground {
|
||||
private async saveNever(tab: chrome.tabs.Tab) {
|
||||
for (let i = this.notificationQueue.length - 1; i >= 0; i--) {
|
||||
const queueMessage = this.notificationQueue[i];
|
||||
if (
|
||||
queueMessage.tab.id !== tab.id ||
|
||||
queueMessage.type !== NotificationQueueMessageType.AddLogin
|
||||
) {
|
||||
if (queueMessage.tab.id !== tab.id || queueMessage.type !== NotificationType.AddLogin) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -1866,7 +1866,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
frameId: this.focusedFieldData.frameId || 0,
|
||||
},
|
||||
);
|
||||
}, 150);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
const NotificationQueueMessageType = {
|
||||
AddLogin: "add",
|
||||
ChangePassword: "change",
|
||||
UnlockVault: "unlock",
|
||||
AtRiskPassword: "at-risk-password",
|
||||
} as const;
|
||||
|
||||
type NotificationQueueMessageTypes =
|
||||
(typeof NotificationQueueMessageType)[keyof typeof NotificationQueueMessageType];
|
||||
|
||||
export { NotificationQueueMessageType, NotificationQueueMessageTypes };
|
||||
10
apps/browser/src/autofill/enums/notification-type.enum.ts
Normal file
10
apps/browser/src/autofill/enums/notification-type.enum.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
const NotificationType = {
|
||||
AddLogin: "add",
|
||||
ChangePassword: "change",
|
||||
UnlockVault: "unlock",
|
||||
AtRiskPassword: "at-risk-password",
|
||||
} as const;
|
||||
|
||||
type NotificationTypes = (typeof NotificationType)[keyof typeof NotificationType];
|
||||
|
||||
export { NotificationType, NotificationTypes };
|
||||
@@ -14,6 +14,10 @@ const NotificationTypes = {
|
||||
AtRiskPassword: "at-risk-password",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* @todo Deprecate in favor of apps/browser/src/autofill/enums/notification-type.enum.ts
|
||||
* - Determine fix or workaround for restricted imports of that file.
|
||||
*/
|
||||
type NotificationType = (typeof NotificationTypes)[keyof typeof NotificationTypes];
|
||||
|
||||
type NotificationTaskInfo = {
|
||||
@@ -21,6 +25,9 @@ type NotificationTaskInfo = {
|
||||
remainingTasksCount: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* @todo Use generics to make this type specific to notification types, see Standard_NotificationQueueMessage.
|
||||
*/
|
||||
type NotificationBarIframeInitData = {
|
||||
ciphers?: NotificationCipherData[];
|
||||
folders?: FolderView[];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { combineLatest, map, Observable, startWith, switchMap } from "rxjs";
|
||||
import { map, Observable, startWith, switchMap } from "rxjs";
|
||||
|
||||
import { NudgesService } from "@bitwarden/angular/vault";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { Icons } from "@bitwarden/components";
|
||||
|
||||
import { NavButton } from "../platform/popup/layout/popup-tab-navigation.component";
|
||||
@@ -19,12 +17,9 @@ export class TabsV2Component {
|
||||
private hasActiveBadges$ = this.accountService.activeAccount$
|
||||
.pipe(getUserId)
|
||||
.pipe(switchMap((userId) => this.nudgesService.hasActiveBadges$(userId)));
|
||||
protected navButtons$: Observable<NavButton[]> = combineLatest([
|
||||
this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge),
|
||||
this.hasActiveBadges$,
|
||||
]).pipe(
|
||||
startWith([false, false]),
|
||||
map(([onboardingFeatureEnabled, hasBadges]) => {
|
||||
protected navButtons$: Observable<NavButton[]> = this.hasActiveBadges$.pipe(
|
||||
startWith(false),
|
||||
map((hasBadges) => {
|
||||
return [
|
||||
{
|
||||
label: "vault",
|
||||
@@ -49,7 +44,7 @@ export class TabsV2Component {
|
||||
page: "/tabs/settings",
|
||||
icon: Icons.SettingsInactive,
|
||||
iconActive: Icons.SettingsActive,
|
||||
showBerry: onboardingFeatureEnabled && hasBadges,
|
||||
showBerry: hasBadges,
|
||||
},
|
||||
];
|
||||
}),
|
||||
@@ -57,6 +52,5 @@ export class TabsV2Component {
|
||||
constructor(
|
||||
private nudgesService: NudgesService,
|
||||
private accountService: AccountService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -23,12 +23,6 @@
|
||||
<i slot="end" class="bwi bwi-external-link" aria-hidden="true"></i>
|
||||
</button>
|
||||
</bit-item>
|
||||
<bit-item *ngIf="!(isNudgeFeatureEnabled$ | async)">
|
||||
<a bit-item-content routerLink="/more-from-bitwarden">
|
||||
{{ "moreFromBitwarden" | i18n }}
|
||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
</bit-item>
|
||||
<bit-item>
|
||||
<button type="button" bit-item-content (click)="rate()">
|
||||
{{ "rateExtension" | i18n }}
|
||||
|
||||
@@ -5,8 +5,6 @@ import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { DeviceType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { DialogService, ItemModule } from "@bitwarden/components";
|
||||
@@ -48,17 +46,12 @@ export class AboutPageV2Component {
|
||||
private dialogService: DialogService,
|
||||
private environmentService: EnvironmentService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
about() {
|
||||
this.dialogService.open(AboutDialogComponent);
|
||||
}
|
||||
|
||||
protected isNudgeFeatureEnabled$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.PM8851_BrowserOnboardingNudge,
|
||||
);
|
||||
|
||||
async launchHelp() {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "continueToHelpCenter" },
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
</bit-item>
|
||||
<bit-item *ngIf="isNudgeFeatureEnabled$ | async">
|
||||
<bit-item>
|
||||
<a bit-item-content routerLink="/download-bitwarden">
|
||||
<i slot="start" class="bwi bwi-mobile" aria-hidden="true"></i>
|
||||
<div class="tw-flex tw-items-center tw-justify-center">
|
||||
@@ -92,7 +92,7 @@
|
||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
</bit-item>
|
||||
<bit-item *ngIf="isNudgeFeatureEnabled$ | async">
|
||||
<bit-item>
|
||||
<a bit-item-content routerLink="/more-from-bitwarden">
|
||||
<i slot="start" class="bwi bwi-filter" aria-hidden="true"></i>
|
||||
{{ "moreFromBitwarden" | i18n }}
|
||||
|
||||
@@ -14,8 +14,6 @@ import {
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { BadgeComponent, ItemModule } from "@bitwarden/components";
|
||||
|
||||
@@ -75,15 +73,10 @@ export class SettingsV2Component implements OnInit {
|
||||
),
|
||||
);
|
||||
|
||||
protected isNudgeFeatureEnabled$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.PM8851_BrowserOnboardingNudge,
|
||||
);
|
||||
|
||||
constructor(
|
||||
private readonly nudgesService: NudgesService,
|
||||
private readonly accountService: AccountService,
|
||||
private readonly autofillBrowserSettingsService: AutofillBrowserSettingsService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
|
||||
@@ -78,14 +78,14 @@ describe("ViewV2Component", () => {
|
||||
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
|
||||
|
||||
const mockCipherService = {
|
||||
get: jest.fn().mockResolvedValue({ decrypt: jest.fn().mockResolvedValue(mockCipher) }),
|
||||
cipherViews$: jest.fn().mockImplementation((userId) => of([mockCipher])),
|
||||
getKeyForCipherKeyDecryption: jest.fn().mockResolvedValue({}),
|
||||
deleteWithServer: jest.fn().mockResolvedValue(undefined),
|
||||
softDeleteWithServer: jest.fn().mockResolvedValue(undefined),
|
||||
decrypt: jest.fn().mockResolvedValue(mockCipher),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
mockCipherService.cipherViews$.mockClear();
|
||||
mockCipherService.deleteWithServer.mockClear();
|
||||
mockCipherService.softDeleteWithServer.mockClear();
|
||||
mockNavigate.mockClear();
|
||||
@@ -162,7 +162,7 @@ describe("ViewV2Component", () => {
|
||||
|
||||
flush(); // Resolve all promises
|
||||
|
||||
expect(mockCipherService.get).toHaveBeenCalledWith("122-333-444", mockUserId);
|
||||
expect(mockCipherService.cipherViews$).toHaveBeenCalledWith(mockUserId);
|
||||
expect(component.cipher).toEqual(mockCipher);
|
||||
}));
|
||||
|
||||
@@ -210,7 +210,7 @@ describe("ViewV2Component", () => {
|
||||
}));
|
||||
|
||||
it('invokes `doAutofill` when action="AUTOFILL_ID"', fakeAsync(() => {
|
||||
params$.next({ action: AUTOFILL_ID });
|
||||
params$.next({ action: AUTOFILL_ID, cipherId: mockCipher.id });
|
||||
|
||||
flush(); // Resolve all promises
|
||||
|
||||
@@ -218,7 +218,7 @@ describe("ViewV2Component", () => {
|
||||
}));
|
||||
|
||||
it('invokes `copy` when action="copy-username"', fakeAsync(() => {
|
||||
params$.next({ action: COPY_USERNAME_ID });
|
||||
params$.next({ action: COPY_USERNAME_ID, cipherId: mockCipher.id });
|
||||
|
||||
flush(); // Resolve all promises
|
||||
|
||||
@@ -226,7 +226,7 @@ describe("ViewV2Component", () => {
|
||||
}));
|
||||
|
||||
it('invokes `copy` when action="copy-password"', fakeAsync(() => {
|
||||
params$.next({ action: COPY_PASSWORD_ID });
|
||||
params$.next({ action: COPY_PASSWORD_ID, cipherId: mockCipher.id });
|
||||
|
||||
flush(); // Resolve all promises
|
||||
|
||||
@@ -234,7 +234,7 @@ describe("ViewV2Component", () => {
|
||||
}));
|
||||
|
||||
it('invokes `copy` when action="copy-totp"', fakeAsync(() => {
|
||||
params$.next({ action: COPY_VERIFICATION_CODE_ID });
|
||||
params$.next({ action: COPY_VERIFICATION_CODE_ID, cipherId: mockCipher.id });
|
||||
|
||||
flush(); // Resolve all promises
|
||||
|
||||
@@ -243,11 +243,13 @@ describe("ViewV2Component", () => {
|
||||
|
||||
it("does not set the cipher until reprompt is complete", fakeAsync(() => {
|
||||
let promptPromise: (val?: unknown) => void;
|
||||
mockCipherService.decrypt.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
...mockCipher,
|
||||
reprompt: CipherRepromptType.Password,
|
||||
}),
|
||||
mockCipherService.cipherViews$.mockImplementationOnce((userId) =>
|
||||
of([
|
||||
{
|
||||
...mockCipher,
|
||||
reprompt: CipherRepromptType.Password,
|
||||
},
|
||||
]),
|
||||
);
|
||||
doAutofill.mockImplementationOnce(() => {
|
||||
return new Promise((resolve) => {
|
||||
@@ -256,7 +258,7 @@ describe("ViewV2Component", () => {
|
||||
});
|
||||
});
|
||||
|
||||
params$.next({ action: AUTOFILL_ID });
|
||||
params$.next({ action: AUTOFILL_ID, cipherId: mockCipher.id });
|
||||
|
||||
flush(); // Flush all pending actions
|
||||
|
||||
@@ -271,11 +273,13 @@ describe("ViewV2Component", () => {
|
||||
|
||||
it("does not set the cipher at all if doAutofill fails and reprompt is active", fakeAsync(() => {
|
||||
let promptPromise: (val?: unknown) => void;
|
||||
mockCipherService.decrypt.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
...mockCipher,
|
||||
reprompt: CipherRepromptType.Password,
|
||||
}),
|
||||
mockCipherService.cipherViews$.mockImplementationOnce((userId) =>
|
||||
of([
|
||||
{
|
||||
...mockCipher,
|
||||
reprompt: CipherRepromptType.Password,
|
||||
},
|
||||
]),
|
||||
);
|
||||
doAutofill.mockImplementationOnce(() => {
|
||||
return new Promise((resolve) => {
|
||||
@@ -284,7 +288,7 @@ describe("ViewV2Component", () => {
|
||||
});
|
||||
});
|
||||
|
||||
params$.next({ action: AUTOFILL_ID });
|
||||
params$.next({ action: AUTOFILL_ID, cipherId: mockCipher.id });
|
||||
|
||||
flush(); // Flush all pending actions
|
||||
|
||||
@@ -301,11 +305,13 @@ describe("ViewV2Component", () => {
|
||||
"does not set cipher when copy fails for %s",
|
||||
fakeAsync((action: string) => {
|
||||
let promptPromise: (val?: unknown) => void;
|
||||
mockCipherService.decrypt.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
...mockCipher,
|
||||
reprompt: CipherRepromptType.Password,
|
||||
}),
|
||||
mockCipherService.cipherViews$.mockImplementationOnce((userId) =>
|
||||
of([
|
||||
{
|
||||
...mockCipher,
|
||||
reprompt: CipherRepromptType.Password,
|
||||
},
|
||||
]),
|
||||
);
|
||||
copy.mockImplementationOnce(() => {
|
||||
return new Promise((resolve) => {
|
||||
@@ -314,7 +320,7 @@ describe("ViewV2Component", () => {
|
||||
});
|
||||
});
|
||||
|
||||
params$.next({ action });
|
||||
params$.next({ action, cipherId: mockCipher.id });
|
||||
|
||||
flush(); // Flush all pending actions
|
||||
|
||||
@@ -336,7 +342,7 @@ describe("ViewV2Component", () => {
|
||||
.spyOn(BrowserApi, "focusTab")
|
||||
.mockImplementation(() => Promise.resolve());
|
||||
|
||||
params$.next({ action: AUTOFILL_ID, senderTabId: 99 });
|
||||
params$.next({ action: AUTOFILL_ID, senderTabId: 99, cipherId: mockCipher.id });
|
||||
|
||||
flush(); // Resolve all promises
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Component } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormsModule } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { firstValueFrom, Observable, switchMap, of } from "rxjs";
|
||||
import { firstValueFrom, Observable, switchMap, of, map } from "rxjs";
|
||||
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
@@ -209,8 +209,12 @@ export class ViewV2Component {
|
||||
}
|
||||
|
||||
async getCipherData(id: string, userId: UserId) {
|
||||
const cipher = await this.cipherService.get(id, userId);
|
||||
return await this.cipherService.decrypt(cipher, userId);
|
||||
return await firstValueFrom(
|
||||
this.cipherService.cipherViews$(userId).pipe(
|
||||
filterOutNullish(),
|
||||
map((ciphers) => ciphers.find((c) => c.id === id)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async editCipher() {
|
||||
|
||||
@@ -1,29 +1,23 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { Router } from "@angular/router";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
import { IntroCarouselService } from "../services/intro-carousel.service";
|
||||
|
||||
import { IntroCarouselGuard } from "./intro-carousel.guard";
|
||||
|
||||
describe("IntroCarouselGuard", () => {
|
||||
let mockConfigService: MockProxy<ConfigService>;
|
||||
const mockIntroCarouselService = {
|
||||
introCarouselState$: of(true),
|
||||
};
|
||||
const createUrlTree = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
mockConfigService = mock<ConfigService>();
|
||||
createUrlTree.mockClear();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: Router, useValue: { createUrlTree } },
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
{
|
||||
provide: IntroCarouselService,
|
||||
useValue: mockIntroCarouselService,
|
||||
@@ -32,22 +26,16 @@ describe("IntroCarouselGuard", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should return true if the feature flag is off", async () => {
|
||||
mockConfigService.getFeatureFlag.mockResolvedValue(false);
|
||||
const result = await TestBed.runInInjectionContext(async () => await IntroCarouselGuard());
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
it("should navigate to intro-carousel route if feature flag is true and dismissed is true", async () => {
|
||||
mockConfigService.getFeatureFlag.mockResolvedValue(true);
|
||||
it("should return true when dismissed is true", async () => {
|
||||
const result = await TestBed.runInInjectionContext(async () => await IntroCarouselGuard());
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should navigate to intro-carousel route if feature flag is true and dismissed is false", async () => {
|
||||
it("should navigate to intro-carousel route when dismissed is false", async () => {
|
||||
TestBed.overrideProvider(IntroCarouselService, {
|
||||
useValue: { introCarouselState$: of(false) },
|
||||
});
|
||||
mockConfigService.getFeatureFlag.mockResolvedValue(true);
|
||||
|
||||
await TestBed.runInInjectionContext(async () => await IntroCarouselGuard());
|
||||
expect(createUrlTree).toHaveBeenCalledWith(["/intro-carousel"]);
|
||||
});
|
||||
|
||||
@@ -2,23 +2,15 @@ import { inject } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
import { IntroCarouselService } from "../services/intro-carousel.service";
|
||||
|
||||
export const IntroCarouselGuard = async () => {
|
||||
const router = inject(Router);
|
||||
const configService = inject(ConfigService);
|
||||
const introCarouselService = inject(IntroCarouselService);
|
||||
|
||||
const hasOnboardingNudgesFlag = await configService.getFeatureFlag(
|
||||
FeatureFlag.PM8851_BrowserOnboardingNudge,
|
||||
);
|
||||
|
||||
const hasIntroCarouselDismissed = await firstValueFrom(introCarouselService.introCarouselState$);
|
||||
|
||||
if (!hasOnboardingNudgesFlag || hasIntroCarouselDismissed) {
|
||||
if (hasIntroCarouselDismissed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { firstValueFrom, map, Observable } from "rxjs";
|
||||
import { map, Observable } from "rxjs";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import {
|
||||
GlobalState,
|
||||
KeyDefinition,
|
||||
@@ -28,17 +26,9 @@ export class IntroCarouselService {
|
||||
map((x) => x ?? false),
|
||||
);
|
||||
|
||||
constructor(
|
||||
private stateProvider: StateProvider,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
constructor(private stateProvider: StateProvider) {}
|
||||
|
||||
async setIntroCarouselDismissed(): Promise<void> {
|
||||
const hasVaultNudgeFlag = await firstValueFrom(
|
||||
this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge),
|
||||
);
|
||||
if (hasVaultNudgeFlag) {
|
||||
await this.introCarouselState.update(() => true);
|
||||
}
|
||||
await this.introCarouselState.update(() => true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +125,10 @@ describe("VaultPopupAutofillService", () => {
|
||||
});
|
||||
|
||||
it("should only fetch the current tab once when subscribed to multiple times", async () => {
|
||||
(BrowserApi.getTabFromCurrentWindow as jest.Mock).mockClear();
|
||||
|
||||
service.refreshCurrentTab();
|
||||
|
||||
const firstTracked = subscribeTo(service.currentAutofillTab$);
|
||||
const secondTracked = subscribeTo(service.currentAutofillTab$);
|
||||
|
||||
@@ -195,6 +199,7 @@ describe("VaultPopupAutofillService", () => {
|
||||
|
||||
// Refresh the current tab so the mockedPageDetails$ are used
|
||||
service.refreshCurrentTab();
|
||||
(service as any)._currentPageDetails$ = of(mockPageDetails);
|
||||
});
|
||||
|
||||
describe("doAutofill()", () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Injectable } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import {
|
||||
combineLatest,
|
||||
debounceTime,
|
||||
firstValueFrom,
|
||||
map,
|
||||
Observable,
|
||||
@@ -164,6 +165,7 @@ export class VaultPopupAutofillService {
|
||||
}),
|
||||
);
|
||||
}),
|
||||
debounceTime(50),
|
||||
shareReplay({ refCount: false, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
|
||||
@@ -225,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) {
|
||||
|
||||
@@ -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() !== "") {
|
||||
@@ -497,9 +497,9 @@ export class GetCommand extends DownloadCommand {
|
||||
}
|
||||
|
||||
const response = await this.apiService.getCollectionAccessDetails(options.organizationId, id);
|
||||
const decCollection = new CollectionView(response);
|
||||
decCollection.name = await this.encryptService.decryptString(
|
||||
new EncString(response.name),
|
||||
const decCollection = await CollectionView.fromCollectionAccessDetails(
|
||||
response,
|
||||
this.encryptService,
|
||||
orgKey,
|
||||
);
|
||||
const groups =
|
||||
|
||||
@@ -211,7 +211,9 @@ export class ListCommand {
|
||||
}
|
||||
const collections = response.data
|
||||
.filter((c) => c.organizationId === options.organizationId)
|
||||
.map((r) => new Collection(new CollectionData(r as ApiCollectionDetailsResponse)));
|
||||
.map((r) =>
|
||||
Collection.fromCollectionData(new CollectionData(r as ApiCollectionDetailsResponse)),
|
||||
);
|
||||
const orgKeys = await firstValueFrom(this.keyService.orgKeys$(userId));
|
||||
if (orgKeys == null) {
|
||||
throw new Error("Organization keys not found.");
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1226,7 +1226,7 @@
|
||||
"message": "Prijava dvostrukom autentifikacijom"
|
||||
},
|
||||
"vaultTimeoutHeader": {
|
||||
"message": "Vault timeout"
|
||||
"message": "Istek trezora"
|
||||
},
|
||||
"vaultTimeout": {
|
||||
"message": "Istek trezora"
|
||||
@@ -1235,7 +1235,7 @@
|
||||
"message": "Vrijeme isteka"
|
||||
},
|
||||
"vaultTimeoutAction1": {
|
||||
"message": "Timeout action"
|
||||
"message": "Radnja nakon isteka"
|
||||
},
|
||||
"vaultTimeoutDesc": {
|
||||
"message": "Odaberi kada će isteći trezor i koja će se radnja izvršiti."
|
||||
@@ -2518,10 +2518,10 @@
|
||||
"message": "Vrijeme isteka premašuje ograničenje koju je postavila tvoja organizacija."
|
||||
},
|
||||
"vaultTimeoutPolicyAffectingOptions": {
|
||||
"message": "Enterprise policy requirements have been applied to your timeout options"
|
||||
"message": "Pravila tvrtke primijenjena su na vrijeme isteka"
|
||||
},
|
||||
"vaultTimeoutPolicyInEffect": {
|
||||
"message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).",
|
||||
"message": "Pravilo tvoje organizacije utječe na istek trezora. Najveće dozvoljeno vrijeme isteka je $HOURS$:$MINUTES$ h.",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
@@ -2534,7 +2534,7 @@
|
||||
}
|
||||
},
|
||||
"vaultTimeoutPolicyMaximumError": {
|
||||
"message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum",
|
||||
"message": "Tvoja organizacija je zadano postavila kraće vrijeme isteka. Najviše: $HOURS$:$MINUTES$",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
@@ -3037,7 +3037,7 @@
|
||||
}
|
||||
},
|
||||
"loginRequestApprovedForEmailOnDevice": {
|
||||
"message": "Login request approved for $EMAIL$ on $DEVICE$",
|
||||
"message": "Prijava za $EMAIL$ potvrđena na uređaju $DEVICE$",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
@@ -3050,17 +3050,17 @@
|
||||
}
|
||||
},
|
||||
"youDeniedLoginAttemptFromAnotherDevice": {
|
||||
"message": "You denied a login attempt from another device. If this was you, try to log in with the device again."
|
||||
"message": "Odbijena je prijava na drugom uređaju. Ako si ovo stvarno ti, pokušaj se ponovno prijaviti uređajem."
|
||||
},
|
||||
"webApp": {
|
||||
"message": "Web app"
|
||||
"message": "Web aplikacija"
|
||||
},
|
||||
"mobile": {
|
||||
"message": "Mobile",
|
||||
"message": "Mobitel",
|
||||
"description": "Mobile app"
|
||||
},
|
||||
"extension": {
|
||||
"message": "Extension",
|
||||
"message": "Proširenje",
|
||||
"description": "Browser extension/addon"
|
||||
},
|
||||
"desktop": {
|
||||
@@ -3075,10 +3075,10 @@
|
||||
"description": "Software Development Kit"
|
||||
},
|
||||
"server": {
|
||||
"message": "Server"
|
||||
"message": "Poslužitelj"
|
||||
},
|
||||
"loginRequest": {
|
||||
"message": "Login request"
|
||||
"message": "Zahtjev za prijavu"
|
||||
},
|
||||
"deviceType": {
|
||||
"message": "Vrsta uređaja"
|
||||
@@ -4067,10 +4067,10 @@
|
||||
}
|
||||
},
|
||||
"showMore": {
|
||||
"message": "Show more"
|
||||
"message": "Prikaži više"
|
||||
},
|
||||
"showLess": {
|
||||
"message": "Show less"
|
||||
"message": "Pokaži manje"
|
||||
},
|
||||
"enableAutotype": {
|
||||
"message": "Omogući automatski unos"
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
"message": "Pielikumi"
|
||||
},
|
||||
"viewItem": {
|
||||
"message": "Skatīt vienumu"
|
||||
"message": "Apskatīt vienumu"
|
||||
},
|
||||
"name": {
|
||||
"message": "Nosaukums"
|
||||
@@ -573,7 +573,7 @@
|
||||
"message": "Ievietot vietrādi starpliktuvē"
|
||||
},
|
||||
"copyVerificationCodeTotp": {
|
||||
"message": "Ievietot Apliecinājuma kodu (TOTP) starpliktuvē"
|
||||
"message": "Ievietot apliecinājuma kodu (TOTP) starpliktuvē"
|
||||
},
|
||||
"copyFieldCipherName": {
|
||||
"message": "Ievietot starpliktuvē $FIELD$, $CIPHERNAME$",
|
||||
@@ -955,10 +955,10 @@
|
||||
"message": "Ievieto savu drošības atslēgu datora USB ligzdā! Ja tai ir poga, pieskaries tai!"
|
||||
},
|
||||
"recoveryCodeDesc": {
|
||||
"message": "Zaudēta piekļuve visiem divpakāpju nodrošinātājiem? Izmanto atkopšanas kodus, lai atspējotu visus sava konta divpakāpju nodrošinātājus!"
|
||||
"message": "Zaudēta piekļuve visiem divpakāpju nodrošinātājiem? Izmanto atkopes kodus, lai atspējotu visus sava konta divpakāpju nodrošinātājus!"
|
||||
},
|
||||
"recoveryCodeTitle": {
|
||||
"message": "Atgūšanas kods"
|
||||
"message": "Atkopes kods"
|
||||
},
|
||||
"authenticatorAppTitle": {
|
||||
"message": "Autentificētāja lietotne"
|
||||
@@ -2425,7 +2425,7 @@
|
||||
"message": "Pēc savas paroles nomainīšanas būs nepieciešams pieteikties ar jauno paroli. Spēkā esošajās sesijās citās ierīcēs stundas laikā notiks atteikšanās."
|
||||
},
|
||||
"accountRecoveryUpdateMasterPasswordSubtitle": {
|
||||
"message": "Jānomaina sava galvenā'parole, lai pabeigtu konta atkopi."
|
||||
"message": "Jānomaina sava galvenā parole, lai pabeigtu konta atkopi."
|
||||
},
|
||||
"updateMasterPasswordSubtitle": {
|
||||
"message": "Galvenā parole neatbilst šīs apvienības prasībām. Jānomaina sava galvenā parole, lai turpinātu."
|
||||
@@ -3015,10 +3015,10 @@
|
||||
"message": "Ir jāuzstāda pieteikšanās ar ierīci Bitwarden lietotnes iestatījumos. Nepieciešama cita iespēja?"
|
||||
},
|
||||
"viewAllLogInOptions": {
|
||||
"message": "Skatīt visas pieteikšanās iespējas"
|
||||
"message": "Apskatīt visas pieteikšanās iespējas"
|
||||
},
|
||||
"viewAllLoginOptions": {
|
||||
"message": "Skatīt visas pieteikšanās iespējas"
|
||||
"message": "Apskatīt visas pieteikšanās iespējas"
|
||||
},
|
||||
"resendNotification": {
|
||||
"message": "Atkārtoti nosūtīt paziņojumu"
|
||||
@@ -3241,7 +3241,7 @@
|
||||
"message": "pašmitināts"
|
||||
},
|
||||
"accessDenied": {
|
||||
"message": "Piekļuve liegta. Nav nepieciešamo atļauju, lai skatītu šo lapu."
|
||||
"message": "Piekļuve liegta. Nav nepieciešamo atļauju, lai apskatītu šo lapu."
|
||||
},
|
||||
"accountSuccessfullyCreated": {
|
||||
"message": "Konts ir veiksmīgi izveidots."
|
||||
@@ -3627,7 +3627,7 @@
|
||||
"description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy"
|
||||
},
|
||||
"startsWithAdvancedOptionWarning": {
|
||||
"message": "\"Sākas ar' ir lietpratējiem paredzēta iespēja ar paaugstinātu piekļuves datu atklāšanas bīstamību.",
|
||||
"message": "“Sākas ar” ir lietpratējiem paredzēta iespēja ar paaugstinātu piekļuves datu atklāšanas bīstamību.",
|
||||
"description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy"
|
||||
},
|
||||
"uriMatchWarningDialogLink": {
|
||||
@@ -3667,7 +3667,7 @@
|
||||
"message": "Kļūda mērķa mapes piešķiršanā."
|
||||
},
|
||||
"viewItemsIn": {
|
||||
"message": "Skatīt $NAME$ vienumus",
|
||||
"message": "Apskatīt $NAME$ vienumus",
|
||||
"description": "Button to view the contents of a folder or collection",
|
||||
"placeholders": {
|
||||
"name": {
|
||||
@@ -3820,7 +3820,7 @@
|
||||
"message": "Atļaut ekrāna tveršanu"
|
||||
},
|
||||
"allowScreenshotsDesc": {
|
||||
"message": "Ļaut Bitwarden darbvirsmas lietotni tvert ekrānuzņēmumos un rādīt attālās darbvirsmas sesijās. Atspējošana liegs piekļuvu atsevišķos ārējos ekrānos."
|
||||
"message": "Ļaut Bitwarden darbvirsmas lietotni tvert ekrānuzņēmumos un rādīt attālās darbvirsmas sesijās. Atspējošana liegs piekļuvi atsevišķos ārējos ekrānos."
|
||||
},
|
||||
"confirmWindowStillVisibleTitle": {
|
||||
"message": "Apstirpināt, ka logs joprojām ir redzams"
|
||||
|
||||
@@ -3171,7 +3171,7 @@
|
||||
"message": "Ważne:"
|
||||
},
|
||||
"accessing": {
|
||||
"message": "Uzyskiwanie dostępu"
|
||||
"message": "Serwer"
|
||||
},
|
||||
"accessTokenUnableToBeDecrypted": {
|
||||
"message": "Zostałeś wylogowany, ponieważ token dostępu nie mógł zostać odszyfrowany. Zaloguj się ponownie, aby rozwiązać ten problem."
|
||||
|
||||
@@ -3623,7 +3623,7 @@
|
||||
"description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item."
|
||||
},
|
||||
"regExAdvancedOptionWarning": {
|
||||
"message": "A \"expressão regular\" é uma opção avançada com um risco acrescido de exposição de credenciais.",
|
||||
"message": "A \"Expressão regular\" é uma opção avançada com um risco acrescido de exposição de credenciais.",
|
||||
"description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy"
|
||||
},
|
||||
"startsWithAdvancedOptionWarning": {
|
||||
|
||||
@@ -2518,10 +2518,10 @@
|
||||
"message": "Ditt valvs tid för timeout överskrider de begränsningar som fastställts av din organisation."
|
||||
},
|
||||
"vaultTimeoutPolicyAffectingOptions": {
|
||||
"message": "Enterprise policy requirements have been applied to your timeout options"
|
||||
"message": "Företagets policykrav har tillämpats på dina tidsgränsalternativ"
|
||||
},
|
||||
"vaultTimeoutPolicyInEffect": {
|
||||
"message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).",
|
||||
"message": "Din organisations policy har fastställt den maximalt tillåtna tidsgränsen för valvet till $HOURS$ timmar och $MINUTES$ minuter.",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
@@ -2534,7 +2534,7 @@
|
||||
}
|
||||
},
|
||||
"vaultTimeoutPolicyMaximumError": {
|
||||
"message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum",
|
||||
"message": "Tidsgränsen överskrider den begränsning som din organisation har ställt in: $HOURS$ timmar och $MINUTES$ minut(er) maximalt",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
|
||||
@@ -324,7 +324,7 @@
|
||||
"message": "Tháng 12"
|
||||
},
|
||||
"ex": {
|
||||
"message": "Ví dụ:",
|
||||
"message": "ví dụ.",
|
||||
"description": "Short abbreviation for 'example'."
|
||||
},
|
||||
"title": {
|
||||
@@ -1390,7 +1390,7 @@
|
||||
"description": "Copy to clipboard"
|
||||
},
|
||||
"checkForUpdates": {
|
||||
"message": "Kiểm tra cập nhật mới"
|
||||
"message": "Kiểm tra cập nhật…"
|
||||
},
|
||||
"version": {
|
||||
"message": "Phiên bản $VERSION_NUM$",
|
||||
@@ -3769,7 +3769,7 @@
|
||||
"message": "Tải lên"
|
||||
},
|
||||
"authorize": {
|
||||
"message": "Uỷ quyền"
|
||||
"message": "Ủy quyền"
|
||||
},
|
||||
"deny": {
|
||||
"message": "Từ chối"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { newGuid } from "@bitwarden/guid";
|
||||
|
||||
import { getNestedCollectionTree, getFlatCollectionTree } from "./collection-utils";
|
||||
|
||||
@@ -9,11 +11,17 @@ describe("CollectionUtils Service", () => {
|
||||
// Arrange
|
||||
const collections: CollectionView[] = [];
|
||||
|
||||
const parentCollection = new CollectionView();
|
||||
parentCollection.name = "Parent";
|
||||
const parentCollection = new CollectionView({
|
||||
name: "Parent",
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
id: newGuid() as CollectionId,
|
||||
});
|
||||
|
||||
const childCollection = new CollectionView();
|
||||
childCollection.name = "Parent/Child";
|
||||
const childCollection = new CollectionView({
|
||||
name: "Parent/Child",
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
id: newGuid() as CollectionId,
|
||||
});
|
||||
|
||||
collections.push(childCollection);
|
||||
collections.push(parentCollection);
|
||||
@@ -41,12 +49,14 @@ describe("CollectionUtils Service", () => {
|
||||
describe("getFlatCollectionTree", () => {
|
||||
it("should flatten a tree node with no children", () => {
|
||||
// Arrange
|
||||
const collection = new CollectionView();
|
||||
collection.name = "Test Collection";
|
||||
collection.id = "test-id";
|
||||
const collection = new CollectionView({
|
||||
name: "Test Collection",
|
||||
id: "test-id" as CollectionId,
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
});
|
||||
|
||||
const treeNodes: TreeNode<CollectionView>[] = [
|
||||
new TreeNode<CollectionView>(collection, null),
|
||||
new TreeNode<CollectionView>(collection, {} as TreeNode<CollectionView>),
|
||||
];
|
||||
|
||||
// Act
|
||||
@@ -59,23 +69,34 @@ describe("CollectionUtils Service", () => {
|
||||
|
||||
it("should flatten a tree node with children", () => {
|
||||
// Arrange
|
||||
const parentCollection = new CollectionView();
|
||||
parentCollection.name = "Parent";
|
||||
parentCollection.id = "parent-id";
|
||||
const parentCollection = new CollectionView({
|
||||
name: "Parent",
|
||||
id: "parent-id" as CollectionId,
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
});
|
||||
|
||||
const child1Collection = new CollectionView();
|
||||
child1Collection.name = "Child 1";
|
||||
child1Collection.id = "child1-id";
|
||||
const child1Collection = new CollectionView({
|
||||
name: "Child 1",
|
||||
id: "child1-id" as CollectionId,
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
});
|
||||
|
||||
const child2Collection = new CollectionView();
|
||||
child2Collection.name = "Child 2";
|
||||
child2Collection.id = "child2-id";
|
||||
const child2Collection = new CollectionView({
|
||||
name: "Child 2",
|
||||
id: "child2-id" as CollectionId,
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
});
|
||||
|
||||
const grandchildCollection = new CollectionView();
|
||||
grandchildCollection.name = "Grandchild";
|
||||
grandchildCollection.id = "grandchild-id";
|
||||
const grandchildCollection = new CollectionView({
|
||||
name: "Grandchild",
|
||||
id: "grandchild-id" as CollectionId,
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
});
|
||||
|
||||
const parentNode = new TreeNode<CollectionView>(parentCollection, null);
|
||||
const parentNode = new TreeNode<CollectionView>(
|
||||
parentCollection,
|
||||
{} as TreeNode<CollectionView>,
|
||||
);
|
||||
const child1Node = new TreeNode<CollectionView>(child1Collection, parentNode);
|
||||
const child2Node = new TreeNode<CollectionView>(child2Collection, parentNode);
|
||||
const grandchildNode = new TreeNode<CollectionView>(grandchildCollection, child1Node);
|
||||
|
||||
@@ -22,7 +22,7 @@ export function getNestedCollectionTree(
|
||||
// Collections need to be cloned because ServiceUtils.nestedTraverse actively
|
||||
// modifies the names of collections.
|
||||
// These changes risk affecting collections store in StateService.
|
||||
const clonedCollections = collections
|
||||
const clonedCollections: CollectionView[] | CollectionAdminView[] = collections
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map(cloneCollection);
|
||||
|
||||
@@ -37,6 +37,21 @@ export function getNestedCollectionTree(
|
||||
return nodes;
|
||||
}
|
||||
|
||||
export function cloneCollection(collection: CollectionView): CollectionView;
|
||||
export function cloneCollection(collection: CollectionAdminView): CollectionAdminView;
|
||||
export function cloneCollection(
|
||||
collection: CollectionView | CollectionAdminView,
|
||||
): CollectionView | CollectionAdminView {
|
||||
let cloned;
|
||||
|
||||
if (collection instanceof CollectionAdminView) {
|
||||
cloned = Object.assign(new CollectionAdminView({ ...collection }), collection);
|
||||
} else {
|
||||
cloned = Object.assign(new CollectionView({ ...collection }), collection);
|
||||
}
|
||||
return cloned;
|
||||
}
|
||||
|
||||
export function getFlatCollectionTree(
|
||||
nodes: TreeNode<CollectionAdminView>[],
|
||||
): CollectionAdminView[];
|
||||
@@ -57,32 +72,3 @@ export function getFlatCollectionTree(
|
||||
return [node.node, ...children];
|
||||
});
|
||||
}
|
||||
|
||||
function cloneCollection(collection: CollectionView): CollectionView;
|
||||
function cloneCollection(collection: CollectionAdminView): CollectionAdminView;
|
||||
function cloneCollection(
|
||||
collection: CollectionView | CollectionAdminView,
|
||||
): CollectionView | CollectionAdminView {
|
||||
let cloned;
|
||||
|
||||
if (collection instanceof CollectionAdminView) {
|
||||
cloned = new CollectionAdminView();
|
||||
cloned.groups = [...collection.groups];
|
||||
cloned.users = [...collection.users];
|
||||
cloned.assigned = collection.assigned;
|
||||
cloned.unmanaged = collection.unmanaged;
|
||||
} else {
|
||||
cloned = new CollectionView();
|
||||
}
|
||||
|
||||
cloned.id = collection.id;
|
||||
cloned.externalId = collection.externalId;
|
||||
cloned.hidePasswords = collection.hidePasswords;
|
||||
cloned.name = collection.name;
|
||||
cloned.organizationId = collection.organizationId;
|
||||
cloned.readOnly = collection.readOnly;
|
||||
cloned.manage = collection.manage;
|
||||
cloned.type = collection.type;
|
||||
|
||||
return cloned;
|
||||
}
|
||||
|
||||
@@ -391,11 +391,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,
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -312,7 +312,9 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
async getCollectionNameMap() {
|
||||
const response = from(this.apiService.getCollections(this.organization.id)).pipe(
|
||||
map((res) =>
|
||||
res.data.map((r) => new Collection(new CollectionData(r as CollectionDetailsResponse))),
|
||||
res.data.map((r) =>
|
||||
Collection.fromCollectionData(new CollectionData(r as CollectionDetailsResponse)),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -399,9 +399,14 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
const collectionView = new CollectionAdminView();
|
||||
collectionView.id = this.params.collectionId;
|
||||
collectionView.organizationId = this.formGroup.controls.selectedOrg.value;
|
||||
const parent = this.formGroup.controls.parent?.value;
|
||||
const collectionView = new CollectionAdminView({
|
||||
id: this.params.collectionId as CollectionId,
|
||||
organizationId: this.formGroup.controls.selectedOrg.value,
|
||||
name: parent
|
||||
? `${parent}/${this.formGroup.controls.name.value}`
|
||||
: this.formGroup.controls.name.value,
|
||||
});
|
||||
collectionView.externalId = this.formGroup.controls.externalId.value;
|
||||
collectionView.groups = this.formGroup.controls.access.value
|
||||
.filter((v) => v.type === AccessItemType.Group)
|
||||
@@ -410,13 +415,6 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
.filter((v) => v.type === AccessItemType.Member)
|
||||
.map(convertToSelectionView);
|
||||
|
||||
const parent = this.formGroup.controls.parent.value;
|
||||
if (parent) {
|
||||
collectionView.name = `${parent}/${this.formGroup.controls.name.value}`;
|
||||
} else {
|
||||
collectionView.name = this.formGroup.controls.name.value;
|
||||
}
|
||||
|
||||
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const savedCollection = await this.collectionAdminService.save(collectionView, userId);
|
||||
|
||||
|
||||
@@ -145,13 +145,13 @@ export class AddAccountCreditDialogComponent {
|
||||
map((cloudRegion) => {
|
||||
switch (this.dialogParams.owner.type) {
|
||||
case "account": {
|
||||
return `user_id=${this.dialogParams.owner.data.id},account_credit=1,region=${cloudRegion}`;
|
||||
return `user_id:${this.dialogParams.owner.data.id},account_credit:1,region:${cloudRegion}`;
|
||||
}
|
||||
case "organization": {
|
||||
return `organization_id=${this.dialogParams.owner.data.id},account_credit=1,region=${cloudRegion}`;
|
||||
return `organization_id:${this.dialogParams.owner.data.id},account_credit:1,region:${cloudRegion}`;
|
||||
}
|
||||
case "provider": {
|
||||
return `provider_id=${this.dialogParams.owner.data.id},account_credit=1,region=${cloudRegion}`;
|
||||
return `provider_id:${this.dialogParams.owner.data.id},account_credit:1,region:${cloudRegion}`;
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -224,7 +224,7 @@ export class VaultItemsComponent<C extends CipherViewLike> {
|
||||
}
|
||||
|
||||
protected canEditCollection(collection: CollectionView): boolean {
|
||||
// Only allow allow deletion if collection editing is enabled and not deleting "Unassigned"
|
||||
// Only allow deletion if collection editing is enabled and not deleting "Unassigned"
|
||||
if (collection.id === Unassigned) {
|
||||
return false;
|
||||
}
|
||||
@@ -235,7 +235,7 @@ export class VaultItemsComponent<C extends CipherViewLike> {
|
||||
}
|
||||
|
||||
protected canDeleteCollection(collection: CollectionView): boolean {
|
||||
// Only allow allow deletion if collection editing is enabled and not deleting "Unassigned"
|
||||
// Only allow deletion if collection editing is enabled and not deleting "Unassigned"
|
||||
if (collection.id === Unassigned) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -262,9 +262,11 @@ export const OrganizationTrash: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
const unassignedCollection = new CollectionAdminView();
|
||||
unassignedCollection.id = Unassigned as CollectionId;
|
||||
unassignedCollection.name = "Unassigned";
|
||||
const unassignedCollection = new CollectionAdminView({
|
||||
id: Unassigned as CollectionId,
|
||||
name: "Unassigned",
|
||||
organizationId: "org id" as OrganizationId,
|
||||
});
|
||||
export const OrganizationTopLevelCollection: Story = {
|
||||
args: {
|
||||
ciphers: [],
|
||||
@@ -327,11 +329,11 @@ function createCipherView(i: number, deleted = false): CipherView {
|
||||
function createCollectionView(i: number): CollectionAdminView {
|
||||
const organization = organizations[i % (organizations.length + 1)];
|
||||
const group = groups[i % (groups.length + 1)];
|
||||
const view = new CollectionAdminView();
|
||||
view.id = `collection-${i}` as CollectionId;
|
||||
view.name = `Collection ${i}`;
|
||||
view.organizationId = organization?.id;
|
||||
view.manage = true;
|
||||
const view = new CollectionAdminView({
|
||||
id: `collection-${i}` as CollectionId,
|
||||
name: `Collection ${i}`,
|
||||
organizationId: organization?.id ?? ("orgId" as OrganizationId),
|
||||
});
|
||||
|
||||
if (group !== undefined) {
|
||||
view.groups = [
|
||||
@@ -344,6 +346,7 @@ function createCollectionView(i: number): CollectionAdminView {
|
||||
];
|
||||
}
|
||||
|
||||
view.manage = true;
|
||||
return view;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
|
||||
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 { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
@@ -368,11 +368,16 @@ describe("vault filter service", () => {
|
||||
orgId: string,
|
||||
type?: CollectionType,
|
||||
): CollectionView {
|
||||
const collection = new CollectionView();
|
||||
collection.id = id;
|
||||
collection.name = name;
|
||||
collection.organizationId = orgId;
|
||||
collection.type = type || CollectionTypes.SharedCollection;
|
||||
const collection = new CollectionView({
|
||||
id: id as CollectionId,
|
||||
name,
|
||||
organizationId: orgId as OrganizationId,
|
||||
});
|
||||
|
||||
if (type) {
|
||||
collection.type = type;
|
||||
}
|
||||
|
||||
return collection;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -12,11 +12,7 @@ import {
|
||||
switchMap,
|
||||
} from "rxjs";
|
||||
|
||||
import {
|
||||
CollectionAdminView,
|
||||
CollectionService,
|
||||
CollectionView,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { sortDefaultCollections } from "@bitwarden/angular/vault/vault-filter/services/vault-filter.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
@@ -38,6 +34,7 @@ import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||
import { COLLAPSED_GROUPINGS } from "@bitwarden/common/vault/services/key-state/collapsed-groupings.state";
|
||||
import { CipherListView } from "@bitwarden/sdk-internal";
|
||||
import { cloneCollection } from "@bitwarden/web-vault/app/admin-console/organizations/collections";
|
||||
|
||||
import {
|
||||
CipherTypeFilter,
|
||||
@@ -253,14 +250,8 @@ export class VaultFilterService implements VaultFilterServiceAbstraction {
|
||||
}
|
||||
|
||||
collections.forEach((c) => {
|
||||
const collectionCopy = new CollectionView() as CollectionFilter;
|
||||
collectionCopy.id = c.id;
|
||||
collectionCopy.organizationId = c.organizationId;
|
||||
const collectionCopy = cloneCollection(new CollectionView({ ...c })) as CollectionFilter;
|
||||
collectionCopy.icon = "bwi-collection-shared";
|
||||
if (c instanceof CollectionAdminView) {
|
||||
collectionCopy.groups = c.groups;
|
||||
collectionCopy.assigned = c.assigned;
|
||||
}
|
||||
const parts = c.name != null ? c.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : [];
|
||||
ServiceUtils.nestedTraverse(nodes, 0, parts, collectionCopy, null, NestingDelimiter);
|
||||
});
|
||||
@@ -274,7 +265,7 @@ export class VaultFilterService implements VaultFilterServiceAbstraction {
|
||||
}
|
||||
|
||||
protected getCollectionFilterHead(): TreeNode<CollectionFilter> {
|
||||
const head = new CollectionView() as CollectionFilter;
|
||||
const head = CollectionView.vaultFilterHead() as CollectionFilter;
|
||||
return new TreeNode<CollectionFilter>(head, null, "collections", "AllCollections");
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// @ts-strict-ignore
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { CollectionId } from "@bitwarden/common/types/guid";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
@@ -224,7 +225,9 @@ describe("VaultFilter", () => {
|
||||
|
||||
it("should return false when filtering by All Collections", () => {
|
||||
const filterFunction = createFilterFunction({
|
||||
selectedCollectionNode: createCollectionFilterNode({ id: "AllCollections" }),
|
||||
selectedCollectionNode: createCollectionFilterNode({
|
||||
id: "AllCollections" as CollectionId,
|
||||
}),
|
||||
});
|
||||
|
||||
const result = filterFunction(cipher);
|
||||
@@ -309,15 +312,12 @@ function createFolderFilterNode(options: Partial<FolderFilter>): TreeNode<Folder
|
||||
function createCollectionFilterNode(
|
||||
options: Partial<CollectionFilter>,
|
||||
): TreeNode<CollectionFilter> {
|
||||
const collection = new CollectionView() as CollectionFilter;
|
||||
collection.id = options.id;
|
||||
collection.name = options.name ?? "";
|
||||
collection.icon = options.icon ?? "";
|
||||
collection.organizationId = options.organizationId;
|
||||
collection.externalId = options.externalId ?? "";
|
||||
collection.readOnly = options.readOnly ?? false;
|
||||
collection.hidePasswords = options.hidePasswords ?? false;
|
||||
return new TreeNode<CollectionFilter>(collection, null);
|
||||
const collection = new CollectionView({
|
||||
name: options.name ?? "Test Name",
|
||||
id: options.id ?? null,
|
||||
organizationId: options.organizationId ?? "Org Id",
|
||||
}) as CollectionFilter;
|
||||
return new TreeNode<CollectionFilter>(collection, {} as TreeNode<CollectionFilter>);
|
||||
}
|
||||
|
||||
function createCipher(options: Partial<CipherView> = {}) {
|
||||
|
||||
@@ -5266,11 +5266,11 @@
|
||||
"message": "Preuzmi privitke"
|
||||
},
|
||||
"sendAccessPasswordTitle": {
|
||||
"message": "Enter the password to view this Send",
|
||||
"message": "Za pregled ovog Senda unesi lozinku",
|
||||
"description": "Title of the Send password authentication screen."
|
||||
},
|
||||
"sendAccessContentTitle": {
|
||||
"message": "View Send",
|
||||
"message": "Pogledaj Send",
|
||||
"description": "Title of the Send view content screen."
|
||||
},
|
||||
"sendAccessUnavailable": {
|
||||
@@ -5434,34 +5434,34 @@
|
||||
"message": "Provedi vlasništvo nad podacima organizacije"
|
||||
},
|
||||
"organizationDataOwnershipDesc": {
|
||||
"message": "Require all items to be owned by an organization, removing the option to store items at the account level.",
|
||||
"message": "Zahtijevaj da su sve stavke u vlasništvu organizacije čime se onemogućuje spremanje stavki na osobnoj razini.",
|
||||
"description": "This is the policy description shown in the policy list."
|
||||
},
|
||||
"organizationDataOwnershipContent": {
|
||||
"message": "All items will be owned and saved to the organization, enabling organization-wide controls, visibility, and reporting. When turned on, a default collection be available for each member to store items. Learn more about managing the ",
|
||||
"message": "Sve će stavke biti u vlasništvu i spremljene u organizaciji, što će omogućiti kontrolu, vidljivost i izvještavanje na razini cijele organizacije. Kada je uključeno, svakom će članu biti dostupna zadana kolekcija za pohranu stavki. Saznaj više o upravljanju ",
|
||||
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'All items will be owned and saved to the organization, enabling organization-wide controls, visibility, and reporting. When turned on, a default collection be available for each member to store items. Learn more about managing the credential lifecycle.'"
|
||||
},
|
||||
"organizationDataOwnershipContentAnchor": {
|
||||
"message": "credential lifecycle",
|
||||
"message": "životnim ciklusom vjerodajnica",
|
||||
"description": "This will be used as a hyperlink"
|
||||
},
|
||||
"organizationDataOwnershipWarningTitle": {
|
||||
"message": "Are you sure you want to proceed?"
|
||||
"message": "Sigurno želiš nastaviti?"
|
||||
},
|
||||
"organizationDataOwnershipWarning1": {
|
||||
"message": "will remain accessible to members"
|
||||
"message": "zadana zbirka će ostati dostupna članovima"
|
||||
},
|
||||
"organizationDataOwnershipWarning2": {
|
||||
"message": "will not be automatically selected when creating new items"
|
||||
"message": "zadana zbirka neće biti automatski odabrana kod stvaranja novih stavki"
|
||||
},
|
||||
"organizationDataOwnershipWarning3": {
|
||||
"message": "cannot be managed from the Admin Console until the user is offboarded"
|
||||
"message": "zadanom zbirkom se neće moći upravljati iz Admin konzole dok se ne ukloni korisnika"
|
||||
},
|
||||
"organizationDataOwnershipWarningContentTop": {
|
||||
"message": "By turning this policy off, the default collection: "
|
||||
"message": "Isključivanjem ovog pravila: "
|
||||
},
|
||||
"organizationDataOwnershipWarningContentBottom": {
|
||||
"message": "Learn more about the ",
|
||||
"message": "Saznaj više o ",
|
||||
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'"
|
||||
},
|
||||
"personalOwnership": {
|
||||
@@ -6196,7 +6196,7 @@
|
||||
"message": "min."
|
||||
},
|
||||
"vaultTimeoutPolicyInEffect": {
|
||||
"message": "Pravila tvoje organizacije utječu na vremenski istek trezora. Najveće dozvoljeno vremensko ograničenje trezora je $HOURS$ sati i $MINUTES$ minuta",
|
||||
"message": "Pravilo tvoje organizacije utječe na istek trezora. Najveće dozvoljeno vrijeme isteka je $HOURS$:$MINUTES$ h.",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
@@ -9498,7 +9498,7 @@
|
||||
"message": "Pošaljite podatke o događajima svojoj Logscale instanci"
|
||||
},
|
||||
"failedToSaveIntegration": {
|
||||
"message": "Failed to save integration. Please try again later."
|
||||
"message": "Spremanje integracije nije uspjelo. Pokušaj ponovno kasnije."
|
||||
},
|
||||
"deviceIdMissing": {
|
||||
"message": "Nedostaje ID uređaja"
|
||||
@@ -9585,10 +9585,10 @@
|
||||
"message": "URL"
|
||||
},
|
||||
"bearerToken": {
|
||||
"message": "Bearer Token"
|
||||
"message": "Token nositelja"
|
||||
},
|
||||
"index": {
|
||||
"message": "Index"
|
||||
"message": "Indeks"
|
||||
},
|
||||
"selectAPlan": {
|
||||
"message": "Odaberi plan"
|
||||
@@ -11002,15 +11002,15 @@
|
||||
"message": "Neograničen broj tajni i projekata"
|
||||
},
|
||||
"providersubscriptionCanceled": {
|
||||
"message": "Subscription canceled"
|
||||
"message": "Pretplata otkazana"
|
||||
},
|
||||
"providersubCanceledmessage": {
|
||||
"message": "To resubscribe, contact Bitwarden Customer Support."
|
||||
"message": "Za ponovnu pretplatu, kontaktiraj Bitwarden korisničku podršku."
|
||||
},
|
||||
"showMore": {
|
||||
"message": "Show more"
|
||||
"message": "Prikaži više"
|
||||
},
|
||||
"showLess": {
|
||||
"message": "Show less"
|
||||
"message": "Pokaži manje"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10841,40 +10841,40 @@
|
||||
"message": "Installa l'estensione"
|
||||
},
|
||||
"addItLater": {
|
||||
"message": "Add it later"
|
||||
"message": "Più tardi"
|
||||
},
|
||||
"cannotAutofillPasswordsWithoutExtensionTitle": {
|
||||
"message": "You can't autofill passwords without the browser extension"
|
||||
"message": "Per l'inserimento automatico delle credenziali è necessaria l'estensione del browser"
|
||||
},
|
||||
"cannotAutofillPasswordsWithoutExtensionDesc": {
|
||||
"message": "Are you sure you don't want to add the extension now?"
|
||||
"message": "Vuoi davvero evitare di aggiungere l'estensione?"
|
||||
},
|
||||
"skipToWebApp": {
|
||||
"message": "Skip to web app"
|
||||
"message": "Vai a Bitwarden Web"
|
||||
},
|
||||
"bitwardenExtensionInstalled": {
|
||||
"message": "Bitwarden extension installed!"
|
||||
"message": "Estensione di Bitwarden installata!"
|
||||
},
|
||||
"openExtensionToAutofill": {
|
||||
"message": "Open the extension to log in and start autofilling."
|
||||
"message": "Apri l'estensione cliccando sul tasto della barra degli strumenti e accedi con i tuoi dati per attivare il riempimento automatico."
|
||||
},
|
||||
"openBitwardenExtension": {
|
||||
"message": "Open Bitwarden extension"
|
||||
"message": "Apri l'estensione di Bitwarden"
|
||||
},
|
||||
"gettingStartedWithBitwardenPart1": {
|
||||
"message": "For tips on getting started with Bitwarden visit the",
|
||||
"message": "Per suggerimenti su come muovere i primi passi con Bitwarden, visita la",
|
||||
"description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'For tips on getting started with Bitwarden visit the Learning Center and Help Center'"
|
||||
},
|
||||
"gettingStartedWithBitwardenPart2": {
|
||||
"message": "Learning Center",
|
||||
"message": "Guida introduttiva",
|
||||
"description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'For tips on getting started with Bitwarden visit the Learning Center and Help Center'"
|
||||
},
|
||||
"gettingStartedWithBitwardenPart3": {
|
||||
"message": "Help Center",
|
||||
"message": "il Manuale di Bitwarden",
|
||||
"description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'For tips on getting started with Bitwarden visit the Learning Center and Help Center'"
|
||||
},
|
||||
"setupExtensionContentAlt": {
|
||||
"message": "With the Bitwarden browser extension you can easily create new logins, access your saved logins directly from your browser toolbar, and sign in to accounts quickly using Bitwarden autofill."
|
||||
"message": "Con l'estensione di Bitwarden puoi salvare facilmente nuovi login, avere sempre a disposizione quelli salvati e accedere rapidamente ai servizi Web grazie al riempimento automatico."
|
||||
},
|
||||
"restart": {
|
||||
"message": "Riavvia"
|
||||
@@ -10903,46 +10903,46 @@
|
||||
"description": "Error message shown when trying to add credit to a trialing organization without a billing address."
|
||||
},
|
||||
"billingAddress": {
|
||||
"message": "Billing address"
|
||||
"message": "Indirizzo di fatturazione"
|
||||
},
|
||||
"addBillingAddress": {
|
||||
"message": "Add billing address"
|
||||
"message": "Aggiungi indirizzo di fatturazione"
|
||||
},
|
||||
"editBillingAddress": {
|
||||
"message": "Edit billing address"
|
||||
"message": "Modifica indirizzo di fatturazione"
|
||||
},
|
||||
"noBillingAddress": {
|
||||
"message": "No address on file."
|
||||
"message": "Nessun indirizzo nel file."
|
||||
},
|
||||
"billingAddressUpdated": {
|
||||
"message": "Your billing address has been updated."
|
||||
"message": "Indirizzo di fatturazione aggiornato."
|
||||
},
|
||||
"paymentDetails": {
|
||||
"message": "Payment details"
|
||||
"message": "Dettagli pagamento"
|
||||
},
|
||||
"paymentMethodUpdated": {
|
||||
"message": "Your payment method has been updated."
|
||||
"message": "Il metodo di pagamento è stato aggiornato."
|
||||
},
|
||||
"bankAccountVerified": {
|
||||
"message": "Your bank account has been verified."
|
||||
"message": "Il conto bancario è stato verificato."
|
||||
},
|
||||
"availableCreditAppliedToInvoice": {
|
||||
"message": "Any available credit will be automatically applied towards invoices generated for this account."
|
||||
"message": "Qualsiasi credito disponibile sarà automaticamente applicato alle fatture generate per questo account."
|
||||
},
|
||||
"mustBePositiveNumber": {
|
||||
"message": "Must be a positive number"
|
||||
"message": "Deve essere un numero positivo"
|
||||
},
|
||||
"cardSecurityCode": {
|
||||
"message": "Card security code"
|
||||
"message": "Codice di sicurezza della carta"
|
||||
},
|
||||
"cardSecurityCodeDescription": {
|
||||
"message": "Card security code, also known as CVV or CVC, is typically a 3 digit number printed on the back of your credit card or 4 digit number printed on the front above your card number."
|
||||
"message": "Il codice di sicurezza della carta, noto anche come CVV o CVC, è tipicamente un numero a 3 cifre stampato sul retro della carta di credito, oppure un numero a 4 cifre stampato sul fronte sopra il numero della tua carta."
|
||||
},
|
||||
"verifyBankAccountWarning": {
|
||||
"message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make a micro-deposit within the next 1-2 business days. Enter the statement descriptor code from this deposit on the Payment Details page to verify the bank account. Failure to verify the bank account will result in a missed payment and your subscription being suspended."
|
||||
"message": "Il pagamento con un conto corrente bancario è disponibile solo per i clienti negli Stati Uniti. Ti sarà richiesto di verificare il tuo conto corrente. Effettueremo un micro-deposito entro i prossimi 1-2 giorni lavorativi. Cerca l'apposito codice nei dettagli della transazione e inseriscilo nella pagina di sottoscrizione del provider per verificare il conto bancario. La mancata verifica del conto bancario comporterà un mancato pagamento e la sospensione dell'abbonamento."
|
||||
},
|
||||
"taxId": {
|
||||
"message": "Tax ID: $TAX_ID$",
|
||||
"message": "CF o P.IVA: $TAX_ID$",
|
||||
"placeholders": {
|
||||
"tax_id": {
|
||||
"content": "$1",
|
||||
@@ -10951,14 +10951,14 @@
|
||||
}
|
||||
},
|
||||
"unpaidInvoices": {
|
||||
"message": "Unpaid invoices"
|
||||
"message": "Fatture non pagate"
|
||||
},
|
||||
"unpaidInvoicesForServiceUser": {
|
||||
"message": "Your subscription has not been paid. Contact your provider administrator to restore service to you and your clients.",
|
||||
"message": "Il tuo abbonamento non è stato pagato. Contatta l'amministratore per ripristinare il servizio a te e ai tuoi clienti.",
|
||||
"description": "A message shown in a non-dismissible dialog to service users of unpaid providers."
|
||||
},
|
||||
"providerSuspended": {
|
||||
"message": "$PROVIDER$ is suspended",
|
||||
"message": "$PROVIDER$ è sospeso",
|
||||
"placeholders": {
|
||||
"provider": {
|
||||
"content": "$1",
|
||||
@@ -10967,11 +10967,11 @@
|
||||
}
|
||||
},
|
||||
"restoreProviderPortalAccessViaCustomerSupport": {
|
||||
"message": "To restore access to your provider portal, contact Bitwarden Customer Support to renew your subscription.",
|
||||
"message": "Per ripristinare l'accesso al portale del tuo provider, contatta il Servizio Clienti Bitwarden per rinnovare l'abbonamento.",
|
||||
"description": "A message shown in a non-dismissible dialog to any user of a suspended providers."
|
||||
},
|
||||
"restoreProviderPortalAccessViaPaymentMethod": {
|
||||
"message": "Your subscription has not been paid. To restore service to you and your clients, add a payment method by $CANCELLATION_DATE$.",
|
||||
"message": "L'abbonamento non risulta pagato. Per ripristinare il servizio a te e ai tuoi clienti, aggiungi un metodo di pagamento entro $CANCELLATION_DATE$.",
|
||||
"placeholders": {
|
||||
"cancellation_date": {
|
||||
"content": "$1",
|
||||
@@ -10981,7 +10981,7 @@
|
||||
"description": "A message shown in a non-dismissible dialog to admins of unpaid providers."
|
||||
},
|
||||
"subscribetoEnterprise": {
|
||||
"message": "Subscribe to $PLAN$",
|
||||
"message": "Abbonati a $PLAN$",
|
||||
"placeholders": {
|
||||
"plan": {
|
||||
"content": "$1",
|
||||
@@ -10990,7 +10990,7 @@
|
||||
}
|
||||
},
|
||||
"subscribeEnterpriseSubtitle": {
|
||||
"message": "Your 7-day $PLAN$ trial starts today. Add a payment method now to continue using these features after your trial ends: ",
|
||||
"message": "La tua prova $PLAN$ di 7 giorni inizia oggi. Aggiungi un metodo di pagamento per continuare a utilizzarne le funzionalità dopo la fine del periodo di prova: ",
|
||||
"placeholders": {
|
||||
"plan": {
|
||||
"content": "$1",
|
||||
@@ -10999,18 +10999,18 @@
|
||||
}
|
||||
},
|
||||
"unlimitedSecretsAndProjects": {
|
||||
"message": "Unlimited secrets and projects"
|
||||
"message": "Segreti e progetti illimitati"
|
||||
},
|
||||
"providersubscriptionCanceled": {
|
||||
"message": "Subscription canceled"
|
||||
"message": "Abbonamento cancellato"
|
||||
},
|
||||
"providersubCanceledmessage": {
|
||||
"message": "To resubscribe, contact Bitwarden Customer Support."
|
||||
"message": "Per ri-iscriverti, contatta il Servizio Clienti Bitwarden."
|
||||
},
|
||||
"showMore": {
|
||||
"message": "Show more"
|
||||
"message": "Mostra di più"
|
||||
},
|
||||
"showLess": {
|
||||
"message": "Show less"
|
||||
"message": "Mostra di meno"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -732,7 +732,7 @@
|
||||
"message": "Labot vienumu"
|
||||
},
|
||||
"viewItem": {
|
||||
"message": "Skatīt vienumu"
|
||||
"message": "Apskatīt vienumu"
|
||||
},
|
||||
"newItemHeader": {
|
||||
"message": "Jauns/a $TYPE$",
|
||||
@@ -1502,10 +1502,10 @@
|
||||
"message": "Atlasīt divpakāpju pieteikšanās veidu"
|
||||
},
|
||||
"recoveryCodeDesc": {
|
||||
"message": "Zaudēta piekļuve visiem divpakāpju pieteikšanās nodrošinātājiem? Jāizmanto atkopšanas kods, lai izslēgtu visus sava konta divpakāpju pieteikšanās nodrošinātājus."
|
||||
"message": "Zaudēta piekļuve visiem divpakāpju pieteikšanās nodrošinātājiem? Jāizmanto atkopes kods, lai izslēgtu visus sava konta divpakāpju pieteikšanās nodrošinātājus."
|
||||
},
|
||||
"recoveryCodeTitle": {
|
||||
"message": "Atgūšanas kods"
|
||||
"message": "Atkopes kods"
|
||||
},
|
||||
"authenticatorAppTitle": {
|
||||
"message": "Autentificētāja lietotne"
|
||||
@@ -2180,7 +2180,7 @@
|
||||
"message": "Ja ir uzstādīta vienotā pieteikšanās vai tas ir iecerēts, identitātes nodrošinātājs jau var būt piemērojis divpakāpju pieteikšanos."
|
||||
},
|
||||
"twoStepLoginRecoveryWarning": {
|
||||
"message": "Divpakāpju pieteikšanās var pastāvīgi liegt piekļuvi Bitwarden kontam. Atkopšanas kods ļauj piekļūt tam gadījumā, kad vairs nav iespējams izmantot ierasto divpakāpju pieteikšanās nodrošinātāju (piemēram, ir pazaudēta ierīce). Bitwarden atbalsts nevarēs palīdzēt, ja tiks pazaudēta piekļuve kontam. Ir ieteicams, ka atkopšanas kods tiek pierakstīts vai izdrukāts un turēts drošā vietā."
|
||||
"message": "Divpakāpju pieteikšanās var pastāvīgi liegt piekļuvi Bitwarden kontam. Atkopes kods ļauj piekļūt tam gadījumā, kad vairs nav iespējams izmantot ierasto divpakāpju pieteikšanās nodrošinātāju (piemēram, ir pazaudēta ierīce). Bitwarden atbalsts nevarēs palīdzēt, ja tiks pazaudēta piekļuve kontam. Ir ieteicams, ka atkopes kods tiek pierakstīts vai izdrukāts un turēts drošā vietā."
|
||||
},
|
||||
"restrictedItemTypePolicy": {
|
||||
"message": "Noņemt karšu vienumu veidu"
|
||||
@@ -2198,7 +2198,7 @@
|
||||
"message": "Vienreizējas izmantošanas atkopes kodu var izmantot, lai izslēgtu divpakāpju pieteikšanos gadījumā, ja tiek zaudēta piekļuve savam divpakāpju pieteikšanās nodrošinātājam. Bitwarden iesaka pierakstīt atkopes kodu un glabāt to drošā vietā."
|
||||
},
|
||||
"viewRecoveryCode": {
|
||||
"message": "Skatīt atkopšanas kodu"
|
||||
"message": "Apskatīt atkopes kodu"
|
||||
},
|
||||
"providers": {
|
||||
"message": "Nodrošinātāji",
|
||||
@@ -2455,10 +2455,10 @@
|
||||
"message": "Platformas ierobežojumu dēļ WebAuth nevar izmantot visās Bitwarden lietotnēs. Ir ieteicams iespējot vēl kādu divpakāpju pieteikšanās nodrošinātāju, lai varētu piekļūt kontam, kad nav iespējams izmantot WebAuth."
|
||||
},
|
||||
"twoFactorRecoveryYourCode": {
|
||||
"message": "Bitwarden divpakāpju pieteikšanās atkopšanas kods"
|
||||
"message": "Bitwarden divpakāpju pieteikšanās atkopes kods"
|
||||
},
|
||||
"twoFactorRecoveryNoCode": {
|
||||
"message": "Vēl nav iespējots neviens divpakāpju pieteikšanās nodrošinātājs. Kad tas būs izdarīts, šeit varēs apskatīt atkopšanas kodu."
|
||||
"message": "Vēl nav iespējots neviens divpakāpju pieteikšanās nodrošinātājs. Kad tas būs izdarīts, šeit varēs apskatīt atkopes kodu."
|
||||
},
|
||||
"printCode": {
|
||||
"message": "Izdrukāt kodu",
|
||||
@@ -2922,7 +2922,7 @@
|
||||
"message": "Lejupielādēt licenci"
|
||||
},
|
||||
"viewBillingToken": {
|
||||
"message": "Skatīt norēķinu pilnvaru"
|
||||
"message": "Apskatīt norēķinu tekstvienību"
|
||||
},
|
||||
"updateLicense": {
|
||||
"message": "Atjaunināt licenci"
|
||||
@@ -3597,13 +3597,13 @@
|
||||
}
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"viewedItemId": {
|
||||
"message": "Skatīts vienums $ID$.",
|
||||
"message": "Apskatīts vienums $ID$.",
|
||||
"placeholders": {
|
||||
"id": {
|
||||
"content": "$1",
|
||||
@@ -3612,7 +3612,7 @@
|
||||
}
|
||||
},
|
||||
"viewedPasswordItemId": {
|
||||
"message": "Skatīta vienuma $ID$ parole.",
|
||||
"message": "Apskatīta vienuma $ID$ parole.",
|
||||
"placeholders": {
|
||||
"id": {
|
||||
"content": "$1",
|
||||
@@ -3621,7 +3621,7 @@
|
||||
}
|
||||
},
|
||||
"viewedHiddenFieldItemId": {
|
||||
"message": "Skatīts vienuma $ID$ slēpts lauks.",
|
||||
"message": "Apskatīts vienuma $ID$ slēpts lauks.",
|
||||
"placeholders": {
|
||||
"id": {
|
||||
"content": "$1",
|
||||
@@ -3630,7 +3630,7 @@
|
||||
}
|
||||
},
|
||||
"viewedCardNumberItemId": {
|
||||
"message": "Skatīts vienuma $ID$ kartes numurs.",
|
||||
"message": "Apskatīts vienuma $ID$ kartes numurs.",
|
||||
"placeholders": {
|
||||
"id": {
|
||||
"content": "$1",
|
||||
@@ -3639,7 +3639,7 @@
|
||||
}
|
||||
},
|
||||
"viewedSecurityCodeItemId": {
|
||||
"message": "Skatīts vienuma $ID$ drošības kods.",
|
||||
"message": "Apskatīts vienuma $ID$ drošības kods.",
|
||||
"placeholders": {
|
||||
"id": {
|
||||
"content": "$1",
|
||||
@@ -3648,7 +3648,7 @@
|
||||
}
|
||||
},
|
||||
"viewCollectionWithName": {
|
||||
"message": "Skatīt krājumu - $NAME$",
|
||||
"message": "Apskatīt krājumu - $NAME$",
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"content": "$1",
|
||||
@@ -4027,7 +4027,7 @@
|
||||
"message": ", lai labotu savu e-pasta adresi."
|
||||
},
|
||||
"view": {
|
||||
"message": "Skatīt"
|
||||
"message": "Apskatīt"
|
||||
},
|
||||
"invalidDateRange": {
|
||||
"message": "Nederīgs datumu apgabals."
|
||||
@@ -4229,7 +4229,7 @@
|
||||
"message": "Atcerēties e-pasta adresi"
|
||||
},
|
||||
"recoverAccountTwoStepDesc": {
|
||||
"message": "Ja kontam nevar piekļūt ar ierastajiem divpakāpju pieteikšanās veidiem, var izmantot atkopšanas kodu, lai atspējotu visus konta divpakāpju pieteikšanās nodrošinātājus."
|
||||
"message": "Ja kontam nevar piekļūt ar ierastajiem divpakāpju pieteikšanās veidiem, var izmantot atkopes kodu, lai atspējotu visus konta divpakāpju pieteikšanās nodrošinātājus."
|
||||
},
|
||||
"logInBelowUsingYourSingleUseRecoveryCode": {
|
||||
"message": "Pieteikties ar vienreizējas izmantošanas atkopes kodu zemāk. Tas kontā izslēgs visus divpakāpju nodrošinātājus."
|
||||
@@ -4323,7 +4323,7 @@
|
||||
}
|
||||
},
|
||||
"viewInvoice": {
|
||||
"message": "Skatīt rēķinu"
|
||||
"message": "Apskatīt rēķinu"
|
||||
},
|
||||
"downloadInvoice": {
|
||||
"message": "Lejupielādēt rēķinu"
|
||||
@@ -4816,7 +4816,7 @@
|
||||
"description": "'OAuth 2.0' is a programming protocol. It should probably not be translated."
|
||||
},
|
||||
"viewApiKey": {
|
||||
"message": "Skatīt API atslēgu"
|
||||
"message": "Apskatīt API atslēgu"
|
||||
},
|
||||
"rotateApiKey": {
|
||||
"message": "Mainīt API atslēgu"
|
||||
@@ -5588,7 +5588,7 @@
|
||||
"message": "Pārvaldīt lietotājus"
|
||||
},
|
||||
"manageAccountRecovery": {
|
||||
"message": "Pārvaldīt konta atkopšanu"
|
||||
"message": "Pārvaldīt konta atkopi"
|
||||
},
|
||||
"disableRequiredError": {
|
||||
"message": "Vispirms pašrocīgi ir jāatspējo nosacījums $POLICYNAME$, lai varētu atspējot šo.",
|
||||
@@ -5697,7 +5697,7 @@
|
||||
"message": "Piešķirt dalībniekiem piekļuvi:"
|
||||
},
|
||||
"viewAndSelectTheMembers": {
|
||||
"message": "skatīt un atlasīt dalībniekus, kuriem piešķirt piekļuvi Noslēpumu pārvaldniekam."
|
||||
"message": "apskatīt un atlasīt dalībniekus, kuriem piešķirt piekļuvi Noslēpumu pārvaldniekam."
|
||||
},
|
||||
"openYourOrganizations": {
|
||||
"message": "Jāatver savas apvienības"
|
||||
@@ -5784,13 +5784,13 @@
|
||||
"message": "Paroles norāde nedrīkst būt tāda pati kā parole."
|
||||
},
|
||||
"enrollAccountRecovery": {
|
||||
"message": "Pieteikties konta atkopšanai"
|
||||
"message": "Pieteikties konta atkopei"
|
||||
},
|
||||
"enrolledAccountRecovery": {
|
||||
"message": "Pieteicies konta atkopšanai"
|
||||
"message": "Pieteicies konta atkopei"
|
||||
},
|
||||
"withdrawAccountRecovery": {
|
||||
"message": "Atsaukt konta atkopšanu"
|
||||
"message": "Atsaukt konta atkopi"
|
||||
},
|
||||
"enrollPasswordResetSuccess": {
|
||||
"message": "Pievienošana bija veiksmīga."
|
||||
@@ -5799,7 +5799,7 @@
|
||||
"message": "Izņemšana bija veiksmīga."
|
||||
},
|
||||
"eventEnrollAccountRecovery": {
|
||||
"message": "Lietotājs $ID$ pieteicās konta atkopšanai.",
|
||||
"message": "Lietotājs $ID$ pieteicās konta atkopei.",
|
||||
"placeholders": {
|
||||
"id": {
|
||||
"content": "$1",
|
||||
@@ -5808,7 +5808,7 @@
|
||||
}
|
||||
},
|
||||
"eventWithdrawAccountRecovery": {
|
||||
"message": "Lietotājs $ID$ atsauca konta atkopšanu.",
|
||||
"message": "Lietotājs $ID$ atsauca konta atkopi.",
|
||||
"placeholders": {
|
||||
"id": {
|
||||
"content": "$1",
|
||||
@@ -5880,7 +5880,7 @@
|
||||
"message": "Ievietošana sarakstā ļaus apvienības pārvaldniekiem mainīt galveno paroli. Vai tiešām ievietot sarakstā?"
|
||||
},
|
||||
"accountRecoveryPolicy": {
|
||||
"message": "Kontu atkopšanas pārvaldība"
|
||||
"message": "Kontu atkopes pārvaldība"
|
||||
},
|
||||
"accountRecoveryPolicyDesc": {
|
||||
"message": "Atkarībā no šifrēšanas veida, var atkopt kontus, kad ir aizmirsta galvenā parole vai pazaudētas uzticamās ierīces."
|
||||
@@ -6008,7 +6008,7 @@
|
||||
"description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'"
|
||||
},
|
||||
"accountRecoveryManageUsers": {
|
||||
"message": "Lietotāju pārvaldīšanai ir jābūt iespējotai arī ar konta atkopšanas pārvaldīšanas atļauju"
|
||||
"message": "Lietotāju pārvaldīšanai ir jābūt iespējotai arī ar konta atkopes pārvaldīšanas atļauju"
|
||||
},
|
||||
"setupProvider": {
|
||||
"message": "Nodrošinātāja iestatīšana"
|
||||
@@ -6148,7 +6148,7 @@
|
||||
"message": "Atjaunināt galveno paroli"
|
||||
},
|
||||
"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."
|
||||
@@ -6683,7 +6683,7 @@
|
||||
"message": "Bezmaksas ar pabalstītājdarbību"
|
||||
},
|
||||
"viewBillingSyncToken": {
|
||||
"message": "Apskatīt norēķinu sinhronizēšanas pilnvaru"
|
||||
"message": "Apskatīt norēķinu sinhronizēšanas tekstvienību"
|
||||
},
|
||||
"generateBillingToken": {
|
||||
"message": "Izveidot norēķinu pilnvaru"
|
||||
@@ -6879,7 +6879,7 @@
|
||||
}
|
||||
},
|
||||
"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."
|
||||
},
|
||||
"masterPassword": {
|
||||
"message": "Galvenā parole"
|
||||
@@ -7549,7 +7549,7 @@
|
||||
"description": "The action to modify an existing project."
|
||||
},
|
||||
"viewProject": {
|
||||
"message": "Skatīt projektu",
|
||||
"message": "Apskatīt projektu",
|
||||
"description": "The action to view details of a project."
|
||||
},
|
||||
"deleteProject": {
|
||||
@@ -7628,7 +7628,7 @@
|
||||
"description": "Title for the action to delete a single service account."
|
||||
},
|
||||
"viewServiceAccount": {
|
||||
"message": "Skatīt pakalpojumu kontu",
|
||||
"message": "Apskatīt pakalpojumu kontu",
|
||||
"description": "Action to view the details of a service account."
|
||||
},
|
||||
"deleteServiceAccountDialogMessage": {
|
||||
@@ -8292,7 +8292,7 @@
|
||||
"message": "Svarīgi:"
|
||||
},
|
||||
"viewAll": {
|
||||
"message": "Skatīt visu"
|
||||
"message": "Apskatīt visu"
|
||||
},
|
||||
"showingPortionOfTotal": {
|
||||
"message": "Rāda $PORTION$ no $TOTAL$",
|
||||
@@ -8554,7 +8554,7 @@
|
||||
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'"
|
||||
},
|
||||
"memberDecryptionOptionTdeDescPart2": {
|
||||
"message": "nosacījums,",
|
||||
"message": "pamatnostādne,",
|
||||
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'"
|
||||
},
|
||||
"memberDecryptionOptionTdeDescLink2": {
|
||||
@@ -8566,11 +8566,11 @@
|
||||
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'"
|
||||
},
|
||||
"memberDecryptionOptionTdeDescLink3": {
|
||||
"message": "kontu atkopšanas pārvaldības",
|
||||
"message": "kontu atkopes pārvaldības",
|
||||
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'"
|
||||
},
|
||||
"memberDecryptionOptionTdeDescPart4": {
|
||||
"message": "nosacījums tiks ieslēgts, kad šī iespēja tiks izmantota.",
|
||||
"message": "pamatnostādne tiks ieslēgta, kad šī iespēja tiks izmantota.",
|
||||
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'"
|
||||
},
|
||||
"orgPermissionsUpdatedMustSetPassword": {
|
||||
@@ -8607,7 +8607,7 @@
|
||||
"message": "Atkopt kontu"
|
||||
},
|
||||
"updatedTempPassword": {
|
||||
"message": "Lietotājs atjaunināja konta atkopšanas izsniegtu paroli."
|
||||
"message": "Lietotājs atjaunināja konta atkopes izsniegtu paroli."
|
||||
},
|
||||
"activatedAccessToSecretsManager": {
|
||||
"message": "Ieslēgta piekļuve Noslēpumu pārvaldniekam",
|
||||
@@ -9002,7 +9002,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": {
|
||||
@@ -9262,7 +9262,7 @@
|
||||
"description": "Title for the action to delete a single machine account."
|
||||
},
|
||||
"viewMachineAccount": {
|
||||
"message": "Skatīt mašīnas kontu",
|
||||
"message": "Apskatīt mašīnas kontu",
|
||||
"description": "Action to view the details of a machine account."
|
||||
},
|
||||
"deleteMachineAccountDialogMessage": {
|
||||
@@ -9543,7 +9543,7 @@
|
||||
}
|
||||
},
|
||||
"smSdkTooltip": {
|
||||
"message": "Skatīt $SDK$ glabātavu",
|
||||
"message": "Apskatīt $SDK$ glabātavu",
|
||||
"placeholders": {
|
||||
"sdk": {
|
||||
"content": "$1",
|
||||
@@ -9561,7 +9561,7 @@
|
||||
}
|
||||
},
|
||||
"smSdkAriaLabel": {
|
||||
"message": "skatīt $SDK$ glabātavu jaunā cilnē.",
|
||||
"message": "apskatīt $SDK$ glabātavu jaunā cilnē.",
|
||||
"placeholders": {
|
||||
"sdk": {
|
||||
"content": "$1",
|
||||
@@ -9657,7 +9657,7 @@
|
||||
"message": "Jāievada informācija par sava uzņēmuma apvienību"
|
||||
},
|
||||
"viewItemsIn": {
|
||||
"message": "Skatīt $NAME$ vienumus",
|
||||
"message": "Apskatīt $NAME$ vienumus",
|
||||
"description": "Button to view the contents of a folder or collection",
|
||||
"placeholders": {
|
||||
"name": {
|
||||
@@ -9691,10 +9691,10 @@
|
||||
}
|
||||
},
|
||||
"viewInfo": {
|
||||
"message": "Skatīt informāciju"
|
||||
"message": "Apskatīt informāciju"
|
||||
},
|
||||
"viewAccess": {
|
||||
"message": "Skatīt piekļuvi"
|
||||
"message": "Apskatīt piekļuvi"
|
||||
},
|
||||
"noCollectionsSelected": {
|
||||
"message": "Nav atlasīts neviens krājums."
|
||||
@@ -9778,7 +9778,7 @@
|
||||
"message": "Apliecināts"
|
||||
},
|
||||
"viewSecret": {
|
||||
"message": "Skatīt noslēpumu"
|
||||
"message": "Apskatīt noslēpumu"
|
||||
},
|
||||
"noClients": {
|
||||
"message": "Nav klientu, ko parādīt"
|
||||
@@ -10189,7 +10189,7 @@
|
||||
"message": "Bezparoles vienotā pieteikšanās (SSO)"
|
||||
},
|
||||
"accountRecovery": {
|
||||
"message": "Konta atkopšana"
|
||||
"message": "Konta atkope"
|
||||
},
|
||||
"customRoles": {
|
||||
"message": "Pielāgotas lomas"
|
||||
|
||||
@@ -8998,7 +8998,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": {
|
||||
|
||||
@@ -1968,7 +1968,7 @@
|
||||
"message": "Đã đóng tài khoản của bạn và đã xóa tất cả những dữ liệu liên quan."
|
||||
},
|
||||
"deleteOrganizationWarning": {
|
||||
"message": "Tổ \u001d\u001d\u001dchức sẽ bị xóa vĩnh viễn và không thể hoàn tác."
|
||||
"message": "Tổ chức sẽ bị xóa vĩnh viễn và không thể hoàn tác."
|
||||
},
|
||||
"myAccount": {
|
||||
"message": "Tài khoản của tôi"
|
||||
@@ -2634,7 +2634,7 @@
|
||||
}
|
||||
},
|
||||
"dataBreachReport": {
|
||||
"message": "Dữ liệu bị rò rĩ"
|
||||
"message": "Rò rỉ dữ liệu"
|
||||
},
|
||||
"breachDesc": {
|
||||
"message": "Tài khoản bị xâm phạm có thể lộ thông tin cá nhân của bạn. Hãy bảo vệ tài khoản bị xâm phạm bằng cách kích hoạt xác thực hai yếu tố (2FA) hoặc tạo mật khẩu mạnh hơn."
|
||||
@@ -2643,7 +2643,7 @@
|
||||
"message": "Kiểm tra các tên đăng nhập hoặc địa chỉ email mà bạn sử dụng."
|
||||
},
|
||||
"checkBreaches": {
|
||||
"message": "Kiểm tra các vi phạm"
|
||||
"message": "Kiểm tra các xâm phạm"
|
||||
},
|
||||
"breachUsernameNotFound": {
|
||||
"message": "$USERNAME$ không được tìm thấy trong bất kỳ vụ rò rỉ dữ liệu nào đã biết.",
|
||||
@@ -2684,10 +2684,10 @@
|
||||
"message": "Người dùng bị ảnh hưởng"
|
||||
},
|
||||
"breachOccurred": {
|
||||
"message": "Vụ vi phạm đã xảy ra"
|
||||
"message": "Vụ xâm phạm đã xảy ra"
|
||||
},
|
||||
"breachReported": {
|
||||
"message": "Sự cố vi phạm đã được báo cáo"
|
||||
"message": "Sự cố xâm phạm đã được báo cáo"
|
||||
},
|
||||
"reportError": {
|
||||
"message": "Đã xảy ra lỗi khi cố gắng tải báo cáo. Vui lòng thử lại"
|
||||
@@ -3137,7 +3137,7 @@
|
||||
"message": "Dành cho doanh nghiệp và các tổ chức nhóm khác."
|
||||
},
|
||||
"planNameTeamsStarter": {
|
||||
"message": "Nhóm Starter"
|
||||
"message": "Gói Teams Starter"
|
||||
},
|
||||
"planNameEnterprise": {
|
||||
"message": "Doanh nghiệp"
|
||||
@@ -3251,7 +3251,7 @@
|
||||
}
|
||||
},
|
||||
"trialSecretsManagerThankYou": {
|
||||
"message": "Cảm ơn bạn đã đăng ký Bitwarden Secrets Manager gói $PLAN$!",
|
||||
"message": "Cảm ơn bạn đã đăng ký Trình quản lý Bí mật Bitwarden gói $PLAN$!",
|
||||
"placeholders": {
|
||||
"plan": {
|
||||
"content": "$1",
|
||||
@@ -5282,7 +5282,7 @@
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"noSendsInList": {
|
||||
"message": "Chưa có mục Gửi.",
|
||||
"message": "Chưa có Send nào.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"emergencyAccess": {
|
||||
@@ -5682,7 +5682,7 @@
|
||||
"message": "Các sản phẩm khác từ Bitwarden"
|
||||
},
|
||||
"requestAccessToSecretsManager": {
|
||||
"message": "Yêu cầu quyền truy cập Trình quản lý bí mật"
|
||||
"message": "Yêu cầu quyền truy cập Trình quản lý Bí mật"
|
||||
},
|
||||
"youNeedApprovalFromYourAdminToTrySecretsManager": {
|
||||
"message": "Bạn cần được quản trị viên phê duyệt để dùng thử Trình quản lý bí mật."
|
||||
@@ -5691,7 +5691,7 @@
|
||||
"message": "Access request for secrets manager email sent to admins."
|
||||
},
|
||||
"requestAccessSMDefaultEmailContent": {
|
||||
"message": "Xin chào,\n\nTôi muốn đề nghị đăng ký sử dụng Bitwarden Secrets Manager cho nhóm của chúng ta. Sự hỗ trợ của bạn sẽ rất đáng trân trọng!\n\nBitwarden Secrets Manager là giải pháp quản lý bí mật được mã hóa đầu cuối, giúp lưu trữ, chia sẻ và triển khai an toàn các thông tin xác thực của máy như khóa API, mật khẩu cơ sở dữ liệu và chứng chỉ xác thực.\n\nSecrets Manager sẽ giúp chúng ta:\n\n* Nâng cao bảo mật\n* Tinh giản quy trình hoạt động\n* Ngăn chặn rò rỉ bí mật tốn kém\n\nĐể yêu cầu dùng thử miễn phí cho nhóm, vui lòng liên hệ với Bitwarden.\n\nCảm ơn bạn đã hỗ trợ!\n"
|
||||
"message": "Xin chào,\n\nTôi muốn đề nghị đăng ký sử dụng Trình quản lý Bí mật Bitwarden cho nhóm của chúng ta. Sự hỗ trợ của bạn sẽ rất đáng trân trọng!\n\nTrình quản lý Bí mật Bitwarden là giải pháp quản lý bí mật được mã hóa đầu cuối, giúp lưu trữ, chia sẻ và triển khai an toàn các thông tin xác thực của máy như khóa API, mật khẩu cơ sở dữ liệu và chứng chỉ xác thực.\n\nSecrets Manager sẽ giúp chúng ta:\n\n* Nâng cao bảo mật\n* Tinh giản quy trình hoạt động\n* Ngăn chặn rò rỉ bí mật tốn kém\n\nĐể yêu cầu dùng thử miễn phí cho nhóm, vui lòng liên hệ với Bitwarden.\n\nCảm ơn bạn đã hỗ trợ!"
|
||||
},
|
||||
"giveMembersAccess": {
|
||||
"message": "Cấp quyền truy cập cho thành viên:"
|
||||
@@ -5964,7 +5964,7 @@
|
||||
"message": "Đã xác nhận thành công"
|
||||
},
|
||||
"bulkReinviteMessage": {
|
||||
"message": "Đã mời lại thành côngvv"
|
||||
"message": "Đã mời lại thành công"
|
||||
},
|
||||
"bulkRemovedMessage": {
|
||||
"message": "Đã xóa thành công"
|
||||
@@ -6081,7 +6081,7 @@
|
||||
"message": "Thêm tổ chức mới"
|
||||
},
|
||||
"myProvider": {
|
||||
"message": "My Provider"
|
||||
"message": "Nhà cung cấp của tôi"
|
||||
},
|
||||
"addOrganizationConfirmation": {
|
||||
"message": "Bạn có chắc chắn muốn thêm $ORGANIZATION$ làm khách hàng của $PROVIDER$ không?",
|
||||
@@ -6752,7 +6752,7 @@
|
||||
"message": "Để thiết lập tổ chĐể thiết lập tổ chức của bạn trên máy chủ riêng, bạn sẽ cần tải lên tệp giấy phép. Để hỗ trợ gói Gia đình miễn phí và các tính năng thanh toán nâng cao cho tổ chức tự lưu trữ của bạn, bạn sẽ cần thiết lập đồng bộ thanh toán.ức của bạn trên máy chủ riêng, bạn sẽ cần tải lên tệp giấy phép. Để hỗ trợ gói Gia đình miễn phí và các tính năng thanh toán nâng cao cho tổ chức tự lưu trữ của bạn, bạn sẽ cần thiết lập đồng bộ thanh toán."
|
||||
},
|
||||
"billingSyncApiKeyRotated": {
|
||||
"message": "Đã xoay mã thông báo"
|
||||
"message": "Token đã được xoay"
|
||||
},
|
||||
"billingSyncKeyDesc": {
|
||||
"message": "Cần mã thông báo đồng bộ thanh toán từ cài đặt đăng ký của tổ chức trên đám mây của bạn để hoàn tất biểu mẫu này."
|
||||
@@ -7042,7 +7042,7 @@
|
||||
}
|
||||
},
|
||||
"awaitingSyncSingular": {
|
||||
"message": "Token đã được xoay vòng cách đây $DAYS$ ngày. Hãy cập nhật token đồng bộ thanh toán trong cài đặt tổ chức tự lưu trữ của bạn.",
|
||||
"message": "Token đã được xoay cách đây $DAYS$ ngày. Hãy cập nhật token đồng bộ thanh toán trong cài đặt tổ chức tự lưu trữ của bạn.",
|
||||
"placeholders": {
|
||||
"days": {
|
||||
"content": "$1",
|
||||
@@ -7051,7 +7051,7 @@
|
||||
}
|
||||
},
|
||||
"awaitingSyncPlural": {
|
||||
"message": "Token đã được xoay vòng cách đây $DAYS$ ngày. Hãy cập nhật token đồng bộ thanh toán trong cài đặt tổ chức tự lưu trữ của bạn.",
|
||||
"message": "Token đã được xoay cách đây $DAYS$ ngày. Hãy cập nhật token đồng bộ thanh toán trong cài đặt tổ chức tự lưu trữ của bạn.",
|
||||
"placeholders": {
|
||||
"days": {
|
||||
"content": "$1",
|
||||
@@ -7754,7 +7754,7 @@
|
||||
}
|
||||
},
|
||||
"deleteProjectInputLabel": {
|
||||
"message": "Nhập $CONFIRM$ để tiếp tục",
|
||||
"message": "Nhập \"$CONFIRM$\" để tiếp tục",
|
||||
"description": "Users are prompted to type 'confirm' to delete a project",
|
||||
"placeholders": {
|
||||
"confirm": {
|
||||
@@ -7815,7 +7815,7 @@
|
||||
"description": "Title for the section displaying access tokens."
|
||||
},
|
||||
"createAccessToken": {
|
||||
"message": "Tạo access token",
|
||||
"message": "Tạo token truy cập",
|
||||
"description": "Button label for creating a new access token."
|
||||
},
|
||||
"expires": {
|
||||
@@ -7831,7 +7831,7 @@
|
||||
"description": "Title to be displayed when there are no access tokens to display in the list."
|
||||
},
|
||||
"accessTokensNoItemsDesc": {
|
||||
"message": "Để bắt đầu, hãy tạo một access token",
|
||||
"message": "Để bắt đầu, hãy tạo một token truy cập",
|
||||
"description": "Message to be displayed when there are no access tokens to display in the list."
|
||||
},
|
||||
"downloadAccessToken": {
|
||||
@@ -7877,7 +7877,7 @@
|
||||
"description": "Toast message after deleting one or multiple access tokens."
|
||||
},
|
||||
"noAccessTokenSelected": {
|
||||
"message": "Không có access token nào được chọn để thu hồi",
|
||||
"message": "Chưa chọn token truy cập để thu hồi",
|
||||
"description": "Toast error message after trying to delete access tokens but not selecting any access tokens."
|
||||
},
|
||||
"submenu": {
|
||||
@@ -8280,7 +8280,7 @@
|
||||
"message": "Tiếp tục sẽ đăng xuất bạn khỏi tất cả các phiên hoạt động. Bạn sẽ cần đăng nhập lại và hoàn tất đăng nhập hai bước (nếu có). Chúng tôi khuyến nghị xuất kho mật khẩu của bạn trước khi thay đổi cài đặt mã hóa để tránh mất dữ liệu."
|
||||
},
|
||||
"secretsManager": {
|
||||
"message": "Quản lý Bí mật"
|
||||
"message": "Trình quản lý Bí mật"
|
||||
},
|
||||
"secretsManagerAccessDescription": {
|
||||
"message": "Kích hoạt quyền truy cập Trình quản lý Bí mật cho người dùng."
|
||||
@@ -8994,7 +8994,7 @@
|
||||
"description": "Label indicating the most common import formats"
|
||||
},
|
||||
"uriMatchDefaultStrategyHint": {
|
||||
"message": "Bitwarden phát hiện khớp đường dẫn (URI) để xác định các đề xuất tự động điền.",
|
||||
"message": "Phát hiện khớp URI là cách Bitwarden xác định các gợi ý tự động điền.",
|
||||
"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": {
|
||||
@@ -9018,7 +9018,7 @@
|
||||
"description": "Warning (should maintain locale-relevant capitalization)"
|
||||
},
|
||||
"maintainYourSubscription": {
|
||||
"message": "Để duy trì gói đăng ký của bạn cho $ORG$,",
|
||||
"message": "Để duy trì gói đăng ký của bạn cho $ORG$, ",
|
||||
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To maintain your subscription for $ORG$, add a payment method.'",
|
||||
"placeholders": {
|
||||
"org": {
|
||||
@@ -9041,7 +9041,7 @@
|
||||
"message": "Cảm ơn bạn đã đăng ký Trình quản lý Bí mật Bitwarden!"
|
||||
},
|
||||
"smFreeTrialConfirmationEmail": {
|
||||
"message": "Chúng tôi đã gửi email xác nhận đến địa chỉ email của bạn tại"
|
||||
"message": "Chúng tôi đã gửi email xác nhận đến địa chỉ email của bạn tại "
|
||||
},
|
||||
"sorryToSeeYouGo": {
|
||||
"message": "Rất tiếc khi thấy bạn rời đi! Hãy giúp cải thiện Bitwarden bằng cách chia sẻ lý do bạn hủy.",
|
||||
@@ -9082,7 +9082,7 @@
|
||||
"message": "Chào mừng bạn đến với ứng dụng web mới được cải tiến. Tìm hiểu thêm về những thay đổi."
|
||||
},
|
||||
"releaseBlog": {
|
||||
"message": "Đọc blog giới thiệu sản phẩm\n"
|
||||
"message": "Đọc blog giới thiệu sản phẩm"
|
||||
},
|
||||
"adminConsole": {
|
||||
"message": "Bảng điều khiển dành cho quản trị viên"
|
||||
@@ -9477,10 +9477,10 @@
|
||||
"description": "This represents the end of a sentence, broken up to include links. The full sentence will be 'Configure SCIM (System for Cross-domain Identity Management) to automatically provision users and groups to Bitwarden using the implementation guide for your Identity Provider"
|
||||
},
|
||||
"bwdc": {
|
||||
"message": "Trình kết nối thư mục Bitwarden\n\n"
|
||||
"message": "Trình kết nối thư mục Bitwarden"
|
||||
},
|
||||
"bwdcDesc": {
|
||||
"message": "Cấu hình Bitwarden Directory Connector để tự động cấp phép người dùng và nhóm bằng cách sử dụng hướng dẫn triển khai cho Nhà cung cấp danh tính của bạn."
|
||||
"message": "Cấu hình Trình kết nối thư mục Bitwarden để tự động cấp phép người dùng và nhóm bằng cách sử dụng hướng dẫn triển khai cho Nhà cung cấp danh tính của bạn."
|
||||
},
|
||||
"eventManagement": {
|
||||
"message": "Quản lý sự kiện"
|
||||
@@ -9543,7 +9543,7 @@
|
||||
}
|
||||
},
|
||||
"smSdkTooltip": {
|
||||
"message": "Xem kho lưu trữ $SDK$ ",
|
||||
"message": "Xem kho lưu trữ $SDK$",
|
||||
"placeholders": {
|
||||
"sdk": {
|
||||
"content": "$1",
|
||||
@@ -9582,7 +9582,7 @@
|
||||
"message": "Tạo một tổ chức khách hàng mới để quản lý với tư cách Nhà cung cấp. Các ghế bổ sung sẽ được phản ánh trong chu kỳ thanh toán tiếp theo."
|
||||
},
|
||||
"url": {
|
||||
"message": "URL"
|
||||
"message": "Địa chỉ (URL)"
|
||||
},
|
||||
"bearerToken": {
|
||||
"message": "Bearer Token"
|
||||
@@ -9833,7 +9833,7 @@
|
||||
"message": "Số vòng lặp KDF cao hơn có thể giúp bảo vệ mật khẩu chính của bạn khỏi bị kẻ tấn công tấn công brute force."
|
||||
},
|
||||
"incrementsOf100,000": {
|
||||
"message": "từng bước 100.000"
|
||||
"message": "tăng theo bội số 100.000"
|
||||
},
|
||||
"smallIncrements": {
|
||||
"message": "bước nhỏ"
|
||||
@@ -9851,7 +9851,7 @@
|
||||
}
|
||||
},
|
||||
"providerReinstate": {
|
||||
"message": "Liên hệ với Bộ phận Hỗ trợ Khách hàng để khôi phục gói đăng ký của bạn."
|
||||
"message": " Liên hệ với Bộ phận Hỗ trợ Khách hàng để khôi phục gói đăng ký của bạn."
|
||||
},
|
||||
"secretPeopleDescription": {
|
||||
"message": "Grant groups or people access to this secret. Permissions set for people will override permissions set by groups."
|
||||
@@ -9973,7 +9973,7 @@
|
||||
"message": "Dữ liệu"
|
||||
},
|
||||
"purchasedSeatsRemoved": {
|
||||
"message": "Đã xóa ghế đã mua"
|
||||
"message": "đã xóa ghế đã mua"
|
||||
},
|
||||
"environmentVariables": {
|
||||
"message": "Biến môi trường"
|
||||
@@ -10069,7 +10069,7 @@
|
||||
"message": "Lưu trữ tại chỗ tùy chọn"
|
||||
},
|
||||
"upgradeFreeOrganization": {
|
||||
"message": "Nâng cấp tổ chức $NAME$ của bạn",
|
||||
"message": "Nâng cấp tổ chức $NAME$ của bạn ",
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"content": "$1",
|
||||
@@ -10216,7 +10216,7 @@
|
||||
"message": "Trình quản lý mật khẩu Bitwarden"
|
||||
},
|
||||
"secretsManagerComplimentaryPasswordManager": {
|
||||
"message": "Gói đăng ký Password Manager miễn phí một năm của bạn sẽ được nâng cấp lên gói đã chọn. Bạn sẽ không bị tính phí cho đến khi thời gian miễn phí kết thúc."
|
||||
"message": "Gói đăng ký Trình quản lý Mật khẩu miễn phí một năm của bạn sẽ được nâng cấp lên gói đã chọn. Bạn sẽ không bị tính phí cho đến khi thời gian miễn phí kết thúc."
|
||||
},
|
||||
"fileSavedToDevice": {
|
||||
"message": "Đã lưu file. Xem mục tải xuống trên thiết bị của bạn."
|
||||
@@ -10704,7 +10704,7 @@
|
||||
"message": "Số ghế được chỉ định vượt quá số ghế có sẵn."
|
||||
},
|
||||
"userkeyRotationDisclaimerEmergencyAccessText": {
|
||||
"message": "Cụm từ vân tay cho $NUM_USERS$ liên hệ mà bạn đã bật quyền truy cập khẩn cấp.",
|
||||
"message": "Cụm từ xác thực cho $NUM_USERS$ liên hệ mà bạn đã bật quyền truy cập khẩn cấp.",
|
||||
"placeholders": {
|
||||
"num_users": {
|
||||
"content": "$1",
|
||||
@@ -10713,7 +10713,7 @@
|
||||
}
|
||||
},
|
||||
"userkeyRotationDisclaimerAccountRecoveryOrgsText": {
|
||||
"message": "Cụm từ vân tay cho tổ chức $ORG_NAME$ mà bạn đã bật khôi phục tài khoản.",
|
||||
"message": "Cụm từ xác thực cho tổ chức $ORG_NAME$ mà bạn đã bật khôi phục tài khoản.",
|
||||
"placeholders": {
|
||||
"org_name": {
|
||||
"content": "$1",
|
||||
|
||||
@@ -49,6 +49,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
@apply tw-bg-background;
|
||||
@apply tw-text-main;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loading page
|
||||
*/
|
||||
|
||||
@@ -10,6 +10,11 @@
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
button {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
h3,
|
||||
button.filter-button {
|
||||
margin: 0;
|
||||
@@ -68,6 +73,12 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
button,
|
||||
a {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.contents {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Subject } from "rxjs";
|
||||
import { filter, firstValueFrom, map, Subject, switchMap } from "rxjs";
|
||||
|
||||
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 { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { ProjectListView } from "../models/view/project-list.view";
|
||||
@@ -29,8 +32,22 @@ export class ProjectService {
|
||||
private keyService: KeyService,
|
||||
private apiService: ApiService,
|
||||
private encryptService: EncryptService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
private getOrganizationKey$(organizationId: string) {
|
||||
return this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.keyService.orgKeys$(userId)),
|
||||
filter((orgKeys) => !!orgKeys),
|
||||
map((organizationKeysById) => organizationKeysById[organizationId as OrganizationId]),
|
||||
);
|
||||
}
|
||||
|
||||
private async getOrganizationKey(organizationId: string): Promise<SymmetricCryptoKey> {
|
||||
return await firstValueFrom(this.getOrganizationKey$(organizationId));
|
||||
}
|
||||
|
||||
async getByProjectId(projectId: string): Promise<ProjectView> {
|
||||
const r = await this.apiService.send("GET", "/projects/" + projectId, null, true, true);
|
||||
const projectResponse = new ProjectResponse(r);
|
||||
@@ -83,10 +100,6 @@ export class ProjectService {
|
||||
});
|
||||
}
|
||||
|
||||
private async getOrganizationKey(organizationId: string): Promise<SymmetricCryptoKey> {
|
||||
return await this.keyService.getOrgKey(organizationId);
|
||||
}
|
||||
|
||||
private async getProjectRequest(
|
||||
organizationId: string,
|
||||
projectView: ProjectView,
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { SecretAccessPoliciesView } from "../models/view/access-policies/secret-access-policies.view";
|
||||
@@ -11,6 +17,16 @@ import { AccessPolicyService } from "../shared/access-policies/access-policy.ser
|
||||
|
||||
import { SecretService } from "./secret.service";
|
||||
|
||||
const SomeCsprngArray = new Uint8Array(64) as CsprngArray;
|
||||
const SomeOrganization = "some organization" as OrganizationId;
|
||||
const AnotherOrganization = "another organization" as OrganizationId;
|
||||
const SomeOrgKey = new SymmetricCryptoKey(SomeCsprngArray) as OrgKey;
|
||||
const AnotherOrgKey = new SymmetricCryptoKey(SomeCsprngArray) as OrgKey;
|
||||
const OrgRecords: Record<OrganizationId, OrgKey> = {
|
||||
[SomeOrganization]: SomeOrgKey,
|
||||
[AnotherOrganization]: AnotherOrgKey,
|
||||
};
|
||||
|
||||
describe("SecretService", () => {
|
||||
let sut: SecretService;
|
||||
|
||||
@@ -18,11 +34,30 @@ describe("SecretService", () => {
|
||||
const apiService = mock<ApiService>();
|
||||
const encryptService = mock<EncryptService>();
|
||||
const accessPolicyService = mock<AccessPolicyService>();
|
||||
let accountService: MockProxy<AccountService> = mock<AccountService>();
|
||||
const activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>({
|
||||
id: "testId" as UserId,
|
||||
email: "test@example.com",
|
||||
emailVerified: true,
|
||||
name: "Test User",
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
sut = new SecretService(keyService, apiService, encryptService, accessPolicyService);
|
||||
const orgKey$ = new BehaviorSubject(OrgRecords);
|
||||
keyService.orgKeys$.mockReturnValue(orgKey$);
|
||||
|
||||
accountService = mock<AccountService>();
|
||||
accountService.activeAccount$ = activeAccountSubject;
|
||||
|
||||
sut = new SecretService(
|
||||
keyService,
|
||||
apiService,
|
||||
encryptService,
|
||||
accessPolicyService,
|
||||
accountService,
|
||||
);
|
||||
|
||||
encryptService.encryptString.mockResolvedValue({
|
||||
encryptedString: "mockEncryptedString",
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Subject } from "rxjs";
|
||||
import { Subject, firstValueFrom, switchMap, map, filter } from "rxjs";
|
||||
|
||||
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 { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { SecretAccessPoliciesView } from "../models/view/access-policies/secret-access-policies.view";
|
||||
@@ -27,7 +30,6 @@ import { SecretResponse } from "./responses/secret.response";
|
||||
})
|
||||
export class SecretService {
|
||||
protected _secret: Subject<SecretView> = new Subject();
|
||||
|
||||
secret$ = this._secret.asObservable();
|
||||
|
||||
constructor(
|
||||
@@ -35,8 +37,22 @@ export class SecretService {
|
||||
private apiService: ApiService,
|
||||
private encryptService: EncryptService,
|
||||
private accessPolicyService: AccessPolicyService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
private getOrganizationKey$(organizationId: string) {
|
||||
return this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.keyService.orgKeys$(userId)),
|
||||
filter((orgKeys) => !!orgKeys),
|
||||
map((organizationKeysById) => organizationKeysById[organizationId as OrganizationId]),
|
||||
);
|
||||
}
|
||||
|
||||
private async getOrganizationKey(organizationId: string): Promise<SymmetricCryptoKey> {
|
||||
return await firstValueFrom(this.getOrganizationKey$(organizationId));
|
||||
}
|
||||
|
||||
async getBySecretId(secretId: string): Promise<SecretView> {
|
||||
const r = await this.apiService.send("GET", "/secrets/" + secretId, null, true, true);
|
||||
const secretResponse = new SecretResponse(r);
|
||||
@@ -154,10 +170,6 @@ export class SecretService {
|
||||
this._secret.next(null);
|
||||
}
|
||||
|
||||
private async getOrganizationKey(organizationId: string): Promise<SymmetricCryptoKey> {
|
||||
return await this.keyService.getOrgKey(organizationId);
|
||||
}
|
||||
|
||||
private async getSecretRequest(
|
||||
organizationId: string,
|
||||
secretView: SecretView,
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Subject } from "rxjs";
|
||||
import { filter, firstValueFrom, map, Subject, switchMap } from "rxjs";
|
||||
|
||||
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 { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { AccessTokenRequest } from "../models/requests/access-token.request";
|
||||
@@ -32,6 +35,7 @@ export class AccessService {
|
||||
private apiService: ApiService,
|
||||
private keyGenerationService: KeyGenerationService,
|
||||
private encryptService: EncryptService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
async getAccessTokens(
|
||||
@@ -50,6 +54,19 @@ export class AccessService {
|
||||
return await this.createAccessTokenViews(organizationId, results.data);
|
||||
}
|
||||
|
||||
private getOrganizationKey$(organizationId: string) {
|
||||
return this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.keyService.orgKeys$(userId)),
|
||||
filter((orgKeys) => !!orgKeys),
|
||||
map((organizationKeysById) => organizationKeysById[organizationId as OrganizationId]),
|
||||
);
|
||||
}
|
||||
|
||||
private async getOrganizationKey(organizationId: string): Promise<SymmetricCryptoKey> {
|
||||
return await firstValueFrom(this.getOrganizationKey$(organizationId));
|
||||
}
|
||||
|
||||
async createAccessToken(
|
||||
organizationId: string,
|
||||
serviceAccountId: string,
|
||||
@@ -117,10 +134,6 @@ export class AccessService {
|
||||
return accessTokenRequest;
|
||||
}
|
||||
|
||||
private async getOrganizationKey(organizationId: string): Promise<SymmetricCryptoKey> {
|
||||
return await this.keyService.getOrgKey(organizationId);
|
||||
}
|
||||
|
||||
private async createAccessTokenViews(
|
||||
organizationId: string,
|
||||
accessTokenResponses: AccessTokenResponse[],
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Subject } from "rxjs";
|
||||
import { filter, firstValueFrom, map, Subject, switchMap } from "rxjs";
|
||||
|
||||
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 { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import {
|
||||
@@ -34,8 +37,22 @@ export class ServiceAccountService {
|
||||
private keyService: KeyService,
|
||||
private apiService: ApiService,
|
||||
private encryptService: EncryptService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
private getOrganizationKey$(organizationId: string) {
|
||||
return this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.keyService.orgKeys$(userId)),
|
||||
filter((orgKeys) => !!orgKeys),
|
||||
map((organizationKeysById) => organizationKeysById[organizationId as OrganizationId]),
|
||||
);
|
||||
}
|
||||
|
||||
private async getOrganizationKey(organizationId: string): Promise<SymmetricCryptoKey> {
|
||||
return await firstValueFrom(this.getOrganizationKey$(organizationId));
|
||||
}
|
||||
|
||||
async getServiceAccounts(
|
||||
organizationId: string,
|
||||
includeAccessToSecrets?: boolean,
|
||||
@@ -129,10 +146,6 @@ export class ServiceAccountService {
|
||||
});
|
||||
}
|
||||
|
||||
private async getOrganizationKey(organizationId: string): Promise<SymmetricCryptoKey> {
|
||||
return await this.keyService.getOrgKey(organizationId);
|
||||
}
|
||||
|
||||
private async getServiceAccountRequest(
|
||||
organizationKey: SymmetricCryptoKey,
|
||||
serviceAccountView: ServiceAccountView,
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@@ -16,17 +19,43 @@ import { SecretsManagerImportedSecretRequest } from "../models/requests/sm-impor
|
||||
|
||||
import { SecretsManagerPortingApiService } from "./sm-porting-api.service";
|
||||
|
||||
const SomeCsprngArray = new Uint8Array(64) as CsprngArray;
|
||||
const SomeOrganization = "some organization" as OrganizationId;
|
||||
const AnotherOrganization = "another organization" as OrganizationId;
|
||||
const SomeOrgKey = new SymmetricCryptoKey(SomeCsprngArray) as OrgKey;
|
||||
const AnotherOrgKey = new SymmetricCryptoKey(SomeCsprngArray) as OrgKey;
|
||||
const OrgRecords: Record<OrganizationId, OrgKey> = {
|
||||
[SomeOrganization]: SomeOrgKey,
|
||||
[AnotherOrganization]: AnotherOrgKey,
|
||||
};
|
||||
|
||||
describe("SecretsManagerPortingApiService", () => {
|
||||
let sut: SecretsManagerPortingApiService;
|
||||
|
||||
const apiService = mock<ApiService>();
|
||||
const encryptService = mock<EncryptService>();
|
||||
const keyService = mock<KeyService>();
|
||||
let accountService: MockProxy<AccountService>;
|
||||
const activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>({
|
||||
id: "testId" as UserId,
|
||||
email: "test@example.com",
|
||||
emailVerified: true,
|
||||
name: "Test User",
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
sut = new SecretsManagerPortingApiService(apiService, encryptService, keyService);
|
||||
const orgKey$ = new BehaviorSubject(OrgRecords);
|
||||
keyService.orgKeys$.mockReturnValue(orgKey$);
|
||||
accountService = mock<AccountService>();
|
||||
accountService.activeAccount$ = activeAccountSubject;
|
||||
sut = new SecretsManagerPortingApiService(
|
||||
apiService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
|
||||
encryptService.encryptString.mockResolvedValue(mockEncryptedString);
|
||||
encryptService.decryptString.mockResolvedValue(mockUnencryptedString);
|
||||
@@ -51,7 +80,6 @@ describe("SecretsManagerPortingApiService", () => {
|
||||
|
||||
it("emits the import successful", async () => {
|
||||
const expectedRequest = toRequest([project1, project2], [secret1, secret2]);
|
||||
|
||||
let subscriptionCount = 0;
|
||||
sut.imports$.subscribe((request) => {
|
||||
expect(request).toBeDefined();
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Subject } from "rxjs";
|
||||
import { filter, firstValueFrom, map, Subject, switchMap } from "rxjs";
|
||||
|
||||
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 { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { SecretsManagerImportError } from "../models/error/sm-import-error";
|
||||
@@ -31,8 +35,22 @@ export class SecretsManagerPortingApiService {
|
||||
private apiService: ApiService,
|
||||
private encryptService: EncryptService,
|
||||
private keyService: KeyService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
private getOrganizationKey$(organizationId: string) {
|
||||
return this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.keyService.orgKeys$(userId)),
|
||||
filter((orgKeys) => !!orgKeys),
|
||||
map((organizationKeysById) => organizationKeysById[organizationId as OrganizationId]),
|
||||
);
|
||||
}
|
||||
|
||||
private async getOrganizationKey(organizationId: string): Promise<SymmetricCryptoKey> {
|
||||
return await firstValueFrom(this.getOrganizationKey$(organizationId));
|
||||
}
|
||||
|
||||
async export(organizationId: string): Promise<string> {
|
||||
const response = await this.apiService.send(
|
||||
"GET",
|
||||
@@ -78,7 +96,7 @@ export class SecretsManagerPortingApiService {
|
||||
const encryptedImport = new SecretsManagerImportRequest();
|
||||
|
||||
try {
|
||||
const orgKey = await this.keyService.getOrgKey(organizationId);
|
||||
const orgKey = await this.getOrganizationKey(organizationId);
|
||||
encryptedImport.projects = [];
|
||||
encryptedImport.secrets = [];
|
||||
|
||||
@@ -120,7 +138,7 @@ export class SecretsManagerPortingApiService {
|
||||
organizationId: string,
|
||||
exportData: SecretsManagerExportResponse,
|
||||
): Promise<SecretsManagerExport> {
|
||||
const orgKey = await this.keyService.getOrgKey(organizationId);
|
||||
const orgKey = await this.getOrganizationKey(organizationId);
|
||||
const decryptedExport = new SecretsManagerExport();
|
||||
decryptedExport.projects = [];
|
||||
decryptedExport.secrets = [];
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@@ -30,17 +33,39 @@ import { ServiceAccountGrantedPoliciesRequest } from "./models/requests/service-
|
||||
|
||||
import { trackEmissions } from "@bitwarden/common/../spec";
|
||||
|
||||
const SomeCsprngArray = new Uint8Array(64) as CsprngArray;
|
||||
const SomeOrganization = "some organization" as OrganizationId;
|
||||
const AnotherOrganization = "another organization" as OrganizationId;
|
||||
const SomeOrgKey = new SymmetricCryptoKey(SomeCsprngArray) as OrgKey;
|
||||
const AnotherOrgKey = new SymmetricCryptoKey(SomeCsprngArray) as OrgKey;
|
||||
const OrgRecords: Record<OrganizationId, OrgKey> = {
|
||||
[SomeOrganization]: SomeOrgKey,
|
||||
[AnotherOrganization]: AnotherOrgKey,
|
||||
};
|
||||
|
||||
describe("AccessPolicyService", () => {
|
||||
let sut: AccessPolicyService;
|
||||
|
||||
const keyService = mock<KeyService>();
|
||||
const apiService = mock<ApiService>();
|
||||
const encryptService = mock<EncryptService>();
|
||||
let accountService: MockProxy<AccountService>;
|
||||
const activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>({
|
||||
id: "testId" as UserId,
|
||||
email: "test@example.com",
|
||||
emailVerified: true,
|
||||
name: "Test User",
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
sut = new AccessPolicyService(keyService, apiService, encryptService);
|
||||
const orgKey$ = new BehaviorSubject(OrgRecords);
|
||||
keyService.orgKeys$.mockReturnValue(orgKey$);
|
||||
|
||||
accountService = mock<AccountService>();
|
||||
accountService.activeAccount$ = activeAccountSubject;
|
||||
sut = new AccessPolicyService(keyService, apiService, encryptService, accountService);
|
||||
});
|
||||
|
||||
it("instantiates", () => {
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Subject } from "rxjs";
|
||||
import { filter, firstValueFrom, map, Subject, switchMap } from "rxjs";
|
||||
|
||||
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 { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import {
|
||||
@@ -62,8 +65,22 @@ export class AccessPolicyService {
|
||||
private keyService: KeyService,
|
||||
protected apiService: ApiService,
|
||||
protected encryptService: EncryptService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
private getOrganizationKey$(organizationId: string) {
|
||||
return this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.keyService.orgKeys$(userId)),
|
||||
filter((orgKeys) => !!orgKeys),
|
||||
map((organizationKeysById) => organizationKeysById[organizationId as OrganizationId]),
|
||||
);
|
||||
}
|
||||
|
||||
private async getOrganizationKey(organizationId: string): Promise<SymmetricCryptoKey> {
|
||||
return await firstValueFrom(this.getOrganizationKey$(organizationId));
|
||||
}
|
||||
|
||||
async getProjectPeopleAccessPolicies(
|
||||
projectId: string,
|
||||
): Promise<ProjectPeopleAccessPoliciesView> {
|
||||
@@ -268,10 +285,6 @@ export class AccessPolicyService {
|
||||
};
|
||||
}
|
||||
|
||||
private async getOrganizationKey(organizationId: string): Promise<SymmetricCryptoKey> {
|
||||
return await this.keyService.getOrgKey(organizationId);
|
||||
}
|
||||
|
||||
private getAccessPolicyRequest(
|
||||
granteeId: string,
|
||||
view: UserAccessPolicyView | GroupAccessPolicyView | ServiceAccountAccessPolicyView,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import { CollectionAccessSelectionView } from "./collection-access-selection.view";
|
||||
import { CollectionAccessDetailsResponse } from "./collection.response";
|
||||
import { CollectionAccessDetailsResponse, CollectionResponse } from "./collection.response";
|
||||
import { CollectionView } from "./collection.view";
|
||||
|
||||
// TODO: this is used to represent the pseudo "Unassigned" collection as well as
|
||||
@@ -24,24 +27,6 @@ export class CollectionAdminView extends CollectionView {
|
||||
*/
|
||||
assigned: boolean = false;
|
||||
|
||||
constructor(response?: CollectionAccessDetailsResponse) {
|
||||
super(response);
|
||||
|
||||
if (!response) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.groups = response.groups
|
||||
? response.groups.map((g) => new CollectionAccessSelectionView(g))
|
||||
: [];
|
||||
|
||||
this.users = response.users
|
||||
? response.users.map((g) => new CollectionAccessSelectionView(g))
|
||||
: [];
|
||||
|
||||
this.assigned = response.assigned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the user can edit a collection (including user and group access) from the Admin Console.
|
||||
*/
|
||||
@@ -115,4 +100,46 @@ export class CollectionAdminView extends CollectionView {
|
||||
get isUnassignedCollection() {
|
||||
return this.id === Unassigned;
|
||||
}
|
||||
|
||||
static async fromCollectionAccessDetails(
|
||||
collection: CollectionAccessDetailsResponse,
|
||||
encryptService: EncryptService,
|
||||
orgKey: OrgKey,
|
||||
): Promise<CollectionAdminView> {
|
||||
const view = new CollectionAdminView({ ...collection });
|
||||
view.name = await encryptService.decryptString(new EncString(view.name), orgKey);
|
||||
view.assigned = collection.assigned;
|
||||
view.readOnly = collection.readOnly;
|
||||
view.hidePasswords = collection.hidePasswords;
|
||||
view.manage = collection.manage;
|
||||
view.unmanaged = collection.unmanaged;
|
||||
view.type = collection.type;
|
||||
view.externalId = collection.externalId;
|
||||
|
||||
view.groups = collection.groups
|
||||
? collection.groups.map((g) => new CollectionAccessSelectionView(g))
|
||||
: [];
|
||||
|
||||
view.users = collection.users
|
||||
? collection.users.map((g) => new CollectionAccessSelectionView(g))
|
||||
: [];
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
static async fromCollectionResponse(
|
||||
collection: CollectionResponse,
|
||||
encryptService: EncryptService,
|
||||
orgKey: OrgKey,
|
||||
): Promise<CollectionAdminView> {
|
||||
const collectionAdminView = new CollectionAdminView({
|
||||
id: collection.id,
|
||||
name: await encryptService.decryptString(new EncString(collection.name), orgKey),
|
||||
organizationId: collection.organizationId,
|
||||
});
|
||||
|
||||
collectionAdminView.externalId = collection.externalId;
|
||||
|
||||
return collectionAdminView;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,10 @@ export class CollectionWithIdRequest extends CollectionRequest {
|
||||
if (collection == null) {
|
||||
return;
|
||||
}
|
||||
super(collection);
|
||||
super({
|
||||
name: collection.name,
|
||||
externalId: collection.externalId,
|
||||
});
|
||||
this.id = collection.id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,18 +2,18 @@ import { Jsonify } from "type-fest";
|
||||
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { CollectionType } from "./collection";
|
||||
import { CollectionType, CollectionTypes } from "./collection";
|
||||
import { CollectionDetailsResponse } from "./collection.response";
|
||||
|
||||
export class CollectionData {
|
||||
id: CollectionId;
|
||||
organizationId: OrganizationId;
|
||||
name: string;
|
||||
externalId: string;
|
||||
readOnly: boolean;
|
||||
manage: boolean;
|
||||
hidePasswords: boolean;
|
||||
type: CollectionType;
|
||||
externalId: string | undefined;
|
||||
readOnly: boolean = false;
|
||||
manage: boolean = false;
|
||||
hidePasswords: boolean = false;
|
||||
type: CollectionType = CollectionTypes.SharedCollection;
|
||||
|
||||
constructor(response: CollectionDetailsResponse) {
|
||||
this.id = response.id;
|
||||
|
||||
@@ -1,20 +1,30 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request";
|
||||
|
||||
import { Collection } from "./collection";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
|
||||
export class CollectionRequest {
|
||||
name: string;
|
||||
externalId: string;
|
||||
externalId: string | undefined;
|
||||
groups: SelectionReadOnlyRequest[] = [];
|
||||
users: SelectionReadOnlyRequest[] = [];
|
||||
|
||||
constructor(collection?: Collection) {
|
||||
if (collection == null) {
|
||||
return;
|
||||
constructor(c: {
|
||||
name: EncString;
|
||||
users?: SelectionReadOnlyRequest[];
|
||||
groups?: SelectionReadOnlyRequest[];
|
||||
externalId?: string;
|
||||
}) {
|
||||
if (!c.name || !c.name.encryptedString) {
|
||||
throw new Error("Name not provided for CollectionRequest.");
|
||||
}
|
||||
|
||||
this.name = c.name.encryptedString;
|
||||
this.externalId = c.externalId;
|
||||
|
||||
if (c.groups) {
|
||||
this.groups = c.groups;
|
||||
}
|
||||
if (c.users) {
|
||||
this.users = c.users;
|
||||
}
|
||||
this.name = collection.name ? collection.name.encryptedString : null;
|
||||
this.externalId = collection.externalId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@ import { SelectionReadOnlyResponse } from "@bitwarden/common/admin-console/model
|
||||
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { CollectionType } from "./collection";
|
||||
import { CollectionType, CollectionTypes } from "./collection";
|
||||
|
||||
export class CollectionResponse extends BaseResponse {
|
||||
id: CollectionId;
|
||||
organizationId: OrganizationId;
|
||||
name: string;
|
||||
externalId: string;
|
||||
type: CollectionType;
|
||||
externalId: string | undefined;
|
||||
type: CollectionType = CollectionTypes.SharedCollection;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
@@ -17,7 +17,7 @@ export class CollectionResponse extends BaseResponse {
|
||||
this.organizationId = this.getResponseProperty("OrganizationId");
|
||||
this.name = this.getResponseProperty("Name");
|
||||
this.externalId = this.getResponseProperty("ExternalId");
|
||||
this.type = this.getResponseProperty("Type");
|
||||
this.type = this.getResponseProperty("Type") ?? CollectionTypes.SharedCollection;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,50 +1,62 @@
|
||||
import { makeSymmetricCryptoKey, mockEnc } from "@bitwarden/common/spec";
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { makeSymmetricCryptoKey } from "@bitwarden/common/spec";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import { Collection, CollectionTypes } from "./collection";
|
||||
import { CollectionData } from "./collection.data";
|
||||
import { CollectionDetailsResponse } from "./collection.response";
|
||||
|
||||
describe("Collection", () => {
|
||||
let data: CollectionData;
|
||||
let encService: MockProxy<EncryptService>;
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
id: "id" as CollectionId,
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
name: "encName",
|
||||
externalId: "extId",
|
||||
readOnly: true,
|
||||
manage: true,
|
||||
hidePasswords: true,
|
||||
type: CollectionTypes.DefaultUserCollection,
|
||||
};
|
||||
data = new CollectionData(
|
||||
new CollectionDetailsResponse({
|
||||
id: "id" as CollectionId,
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
name: "encName",
|
||||
externalId: "extId",
|
||||
readOnly: true,
|
||||
manage: true,
|
||||
hidePasswords: true,
|
||||
type: CollectionTypes.DefaultUserCollection,
|
||||
}),
|
||||
);
|
||||
encService = mock<EncryptService>();
|
||||
encService.decryptString.mockResolvedValue("encName");
|
||||
});
|
||||
|
||||
it("Convert from empty", () => {
|
||||
const data = new CollectionData({} as any);
|
||||
const card = new Collection(data);
|
||||
|
||||
expect(card).toEqual({
|
||||
externalId: null,
|
||||
hidePasswords: null,
|
||||
id: null,
|
||||
name: null,
|
||||
organizationId: null,
|
||||
readOnly: null,
|
||||
manage: null,
|
||||
type: null,
|
||||
it("Convert from partial", () => {
|
||||
const card = new Collection({
|
||||
name: new EncString("name"),
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
id: "id" as CollectionId,
|
||||
});
|
||||
expect(() => card).not.toThrow();
|
||||
|
||||
expect(card.name).not.toBe(null);
|
||||
expect(card.organizationId).not.toBe(null);
|
||||
expect(card.id).not.toBe(null);
|
||||
expect(card.externalId).toBe(undefined);
|
||||
expect(card.readOnly).toBe(false);
|
||||
expect(card.manage).toBe(false);
|
||||
expect(card.hidePasswords).toBe(false);
|
||||
expect(card.type).toEqual(CollectionTypes.SharedCollection);
|
||||
});
|
||||
|
||||
it("Convert", () => {
|
||||
const collection = new Collection(data);
|
||||
const collection = Collection.fromCollectionData(data);
|
||||
|
||||
expect(collection).toEqual({
|
||||
id: "id",
|
||||
organizationId: "orgId",
|
||||
name: { encryptedString: "encName", encryptionType: 0 },
|
||||
externalId: { encryptedString: "extId", encryptionType: 0 },
|
||||
externalId: "extId",
|
||||
readOnly: true,
|
||||
manage: true,
|
||||
hidePasswords: true,
|
||||
@@ -53,10 +65,11 @@ describe("Collection", () => {
|
||||
});
|
||||
|
||||
it("Decrypt", async () => {
|
||||
const collection = new Collection();
|
||||
collection.id = "id" as CollectionId;
|
||||
collection.organizationId = "orgId" as OrganizationId;
|
||||
collection.name = mockEnc("encName");
|
||||
const collection = new Collection({
|
||||
name: new EncString("encName"),
|
||||
organizationId: "orgId" as OrganizationId,
|
||||
id: "id" as CollectionId,
|
||||
});
|
||||
collection.externalId = "extId";
|
||||
collection.readOnly = false;
|
||||
collection.hidePasswords = false;
|
||||
@@ -65,7 +78,7 @@ describe("Collection", () => {
|
||||
|
||||
const key = makeSymmetricCryptoKey<OrgKey>();
|
||||
|
||||
const view = await collection.decrypt(key);
|
||||
const view = await collection.decrypt(key, encService);
|
||||
|
||||
expect(view).toEqual({
|
||||
externalId: "extId",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import Domain, { EncryptableKeys } from "@bitwarden/common/platform/models/domain/domain-base";
|
||||
import Domain from "@bitwarden/common/platform/models/domain/domain-base";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
|
||||
@@ -14,45 +15,68 @@ export const CollectionTypes = {
|
||||
export type CollectionType = (typeof CollectionTypes)[keyof typeof CollectionTypes];
|
||||
|
||||
export class Collection extends Domain {
|
||||
id: CollectionId | undefined;
|
||||
organizationId: OrganizationId | undefined;
|
||||
name: EncString | undefined;
|
||||
id: CollectionId;
|
||||
organizationId: OrganizationId;
|
||||
name: EncString;
|
||||
externalId: string | undefined;
|
||||
readOnly: boolean = false;
|
||||
hidePasswords: boolean = false;
|
||||
manage: boolean = false;
|
||||
type: CollectionType = CollectionTypes.SharedCollection;
|
||||
|
||||
constructor(obj?: CollectionData | null) {
|
||||
constructor(c: { id: CollectionId; name: EncString; organizationId: OrganizationId }) {
|
||||
super();
|
||||
if (obj == null) {
|
||||
return;
|
||||
this.id = c.id;
|
||||
this.name = c.name;
|
||||
this.organizationId = c.organizationId;
|
||||
}
|
||||
|
||||
static fromCollectionData(obj: CollectionData): Collection {
|
||||
if (obj == null || obj.name == null || obj.organizationId == null) {
|
||||
throw new Error("CollectionData must contain name and organizationId.");
|
||||
}
|
||||
|
||||
this.buildDomainModel(
|
||||
this,
|
||||
obj,
|
||||
{
|
||||
id: null,
|
||||
organizationId: null,
|
||||
name: null,
|
||||
externalId: null,
|
||||
readOnly: null,
|
||||
hidePasswords: null,
|
||||
manage: null,
|
||||
type: null,
|
||||
},
|
||||
["id", "organizationId", "readOnly", "hidePasswords", "manage", "type"],
|
||||
);
|
||||
const collection = new Collection({
|
||||
...obj,
|
||||
name: new EncString(obj.name),
|
||||
});
|
||||
|
||||
collection.externalId = obj.externalId;
|
||||
collection.readOnly = obj.readOnly;
|
||||
collection.hidePasswords = obj.hidePasswords;
|
||||
collection.manage = obj.manage;
|
||||
collection.type = obj.type;
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
decrypt(orgKey: OrgKey): Promise<CollectionView> {
|
||||
return this.decryptObj<Collection, CollectionView>(
|
||||
this,
|
||||
new CollectionView(this),
|
||||
["name"] as EncryptableKeys<Collection, CollectionView>[],
|
||||
this.organizationId ?? null,
|
||||
orgKey,
|
||||
);
|
||||
static async fromCollectionView(
|
||||
view: CollectionView,
|
||||
encryptService: EncryptService,
|
||||
orgKey: OrgKey,
|
||||
): Promise<Collection> {
|
||||
const collection = new Collection({
|
||||
name: await encryptService.encryptString(view.name, orgKey),
|
||||
id: view.id,
|
||||
organizationId: view.organizationId,
|
||||
});
|
||||
|
||||
collection.externalId = view.externalId;
|
||||
collection.readOnly = view.readOnly;
|
||||
collection.hidePasswords = view.hidePasswords;
|
||||
collection.manage = view.manage;
|
||||
collection.type = view.type;
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
decrypt(orgKey: OrgKey, encryptService: EncryptService): Promise<CollectionView> {
|
||||
return CollectionView.fromCollection(this, encryptService, orgKey);
|
||||
}
|
||||
|
||||
// @TODO: This would be better off in Collection.Utils. Move this there when
|
||||
// refactoring to a shared lib.
|
||||
static isCollectionId(id: any): id is CollectionId {
|
||||
return typeof id === "string" && id != null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { View } from "@bitwarden/common/models/view/view";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { ITreeNodeObject } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
|
||||
import { Collection, CollectionType, CollectionTypes } from "./collection";
|
||||
@@ -11,9 +14,9 @@ import { CollectionAccessDetailsResponse } from "./collection.response";
|
||||
export const NestingDelimiter = "/";
|
||||
|
||||
export class CollectionView implements View, ITreeNodeObject {
|
||||
id: CollectionId | undefined;
|
||||
organizationId: OrganizationId | undefined;
|
||||
name: string = "";
|
||||
id: CollectionId;
|
||||
organizationId: OrganizationId;
|
||||
name: string;
|
||||
externalId: string | undefined;
|
||||
// readOnly applies to the items within a collection
|
||||
readOnly: boolean = false;
|
||||
@@ -22,24 +25,10 @@ export class CollectionView implements View, ITreeNodeObject {
|
||||
assigned: boolean = false;
|
||||
type: CollectionType = CollectionTypes.SharedCollection;
|
||||
|
||||
constructor(c?: Collection | CollectionAccessDetailsResponse) {
|
||||
if (!c) {
|
||||
return;
|
||||
}
|
||||
|
||||
constructor(c: { id: CollectionId; organizationId: OrganizationId; name: string }) {
|
||||
this.id = c.id;
|
||||
this.organizationId = c.organizationId;
|
||||
this.externalId = c.externalId;
|
||||
if (c instanceof Collection) {
|
||||
this.readOnly = c.readOnly;
|
||||
this.hidePasswords = c.hidePasswords;
|
||||
this.manage = c.manage;
|
||||
this.assigned = true;
|
||||
}
|
||||
if (c instanceof CollectionAccessDetailsResponse) {
|
||||
this.assigned = c.assigned;
|
||||
}
|
||||
this.type = c.type;
|
||||
this.name = c.name;
|
||||
}
|
||||
|
||||
canEditItems(org: Organization): boolean {
|
||||
@@ -94,11 +83,56 @@ export class CollectionView implements View, ITreeNodeObject {
|
||||
return false;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Jsonify<CollectionView>) {
|
||||
return Object.assign(new CollectionView(new Collection()), obj);
|
||||
}
|
||||
|
||||
get isDefaultCollection() {
|
||||
return this.type == CollectionTypes.DefaultUserCollection;
|
||||
}
|
||||
|
||||
// FIXME: we should not use a CollectionView object for the vault filter header because it is not a real
|
||||
// CollectionView and this violates ts-strict rules.
|
||||
static vaultFilterHead(): CollectionView {
|
||||
return new CollectionView({
|
||||
id: "" as CollectionId,
|
||||
organizationId: "" as OrganizationId,
|
||||
name: "",
|
||||
});
|
||||
}
|
||||
|
||||
static async fromCollection(
|
||||
collection: Collection,
|
||||
encryptService: EncryptService,
|
||||
key: OrgKey,
|
||||
): Promise<CollectionView> {
|
||||
const view = new CollectionView({ ...collection, name: "" });
|
||||
|
||||
view.name = await encryptService.decryptString(collection.name, key);
|
||||
view.assigned = true;
|
||||
view.externalId = collection.externalId;
|
||||
view.readOnly = collection.readOnly;
|
||||
view.hidePasswords = collection.hidePasswords;
|
||||
view.manage = collection.manage;
|
||||
view.type = collection.type;
|
||||
return view;
|
||||
}
|
||||
|
||||
static async fromCollectionAccessDetails(
|
||||
collection: CollectionAccessDetailsResponse,
|
||||
encryptService: EncryptService,
|
||||
orgKey: OrgKey,
|
||||
): Promise<CollectionView> {
|
||||
const view = new CollectionView({ ...collection });
|
||||
|
||||
view.name = await encryptService.decryptString(new EncString(collection.name), orgKey);
|
||||
view.externalId = collection.externalId;
|
||||
view.type = collection.type;
|
||||
view.assigned = collection.assigned;
|
||||
return view;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Jsonify<CollectionView>) {
|
||||
return Object.assign(new CollectionView({ ...obj }), obj);
|
||||
}
|
||||
|
||||
encrypt(orgKey: OrgKey, encryptService: EncryptService): Promise<Collection> {
|
||||
return Collection.fromCollectionView(this, encryptService, orgKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,13 +9,14 @@ import { CollectionId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { CollectionData, CollectionView } from "../models";
|
||||
|
||||
export const ENCRYPTED_COLLECTION_DATA_KEY = UserKeyDefinition.record<
|
||||
CollectionData | null,
|
||||
CollectionId
|
||||
>(COLLECTION_DISK, "collections", {
|
||||
deserializer: (jsonData: Jsonify<CollectionData | null>) => CollectionData.fromJSON(jsonData),
|
||||
clearOn: ["logout"],
|
||||
});
|
||||
export const ENCRYPTED_COLLECTION_DATA_KEY = UserKeyDefinition.record<CollectionData, CollectionId>(
|
||||
COLLECTION_DISK,
|
||||
"collections",
|
||||
{
|
||||
deserializer: (jsonData: Jsonify<CollectionData>) => CollectionData.fromJSON(jsonData),
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
);
|
||||
|
||||
export const DECRYPTED_COLLECTION_DATA_KEY = new UserKeyDefinition<CollectionView[] | null>(
|
||||
COLLECTION_MEMORY,
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
|
||||
import { combineLatest, firstValueFrom, from, map, Observable, of, switchMap } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
@@ -36,12 +32,15 @@ export class DefaultCollectionAdminService implements CollectionAdminService {
|
||||
this.keyService.orgKeys$(userId),
|
||||
from(this.apiService.getManyCollectionsWithAccessDetails(organizationId)),
|
||||
]).pipe(
|
||||
switchMap(([orgKey, res]) => {
|
||||
switchMap(([orgKeys, res]) => {
|
||||
if (res?.data == null || res.data.length === 0) {
|
||||
return of([]);
|
||||
}
|
||||
if (orgKeys == null) {
|
||||
throw new Error("No org keys found.");
|
||||
}
|
||||
|
||||
return this.decryptMany(organizationId, res.data, orgKey);
|
||||
return this.decryptMany(organizationId, res.data, orgKeys);
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -104,55 +103,65 @@ export class DefaultCollectionAdminService implements CollectionAdminService {
|
||||
orgKeys: Record<OrganizationId, OrgKey>,
|
||||
): Promise<CollectionAdminView[]> {
|
||||
const promises = collections.map(async (c) => {
|
||||
const view = new CollectionAdminView();
|
||||
view.id = c.id;
|
||||
view.name = await this.encryptService.decryptString(
|
||||
new EncString(c.name),
|
||||
orgKeys[organizationId as OrganizationId],
|
||||
);
|
||||
view.externalId = c.externalId;
|
||||
view.organizationId = c.organizationId;
|
||||
|
||||
if (isCollectionAccessDetailsResponse(c)) {
|
||||
view.groups = c.groups;
|
||||
view.users = c.users;
|
||||
view.assigned = c.assigned;
|
||||
view.readOnly = c.readOnly;
|
||||
view.hidePasswords = c.hidePasswords;
|
||||
view.manage = c.manage;
|
||||
view.unmanaged = c.unmanaged;
|
||||
return CollectionAdminView.fromCollectionAccessDetails(
|
||||
c,
|
||||
this.encryptService,
|
||||
orgKeys[organizationId as OrganizationId],
|
||||
);
|
||||
}
|
||||
|
||||
return view;
|
||||
return await CollectionAdminView.fromCollectionResponse(
|
||||
c,
|
||||
this.encryptService,
|
||||
orgKeys[organizationId as OrganizationId],
|
||||
);
|
||||
});
|
||||
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
|
||||
private async encrypt(model: CollectionAdminView, userId: UserId): Promise<CollectionRequest> {
|
||||
if (model.organizationId == null) {
|
||||
if (!model.organizationId) {
|
||||
throw new Error("Collection has no organization id.");
|
||||
}
|
||||
|
||||
const key = await firstValueFrom(
|
||||
this.keyService
|
||||
.orgKeys$(userId)
|
||||
.pipe(map((orgKeys) => orgKeys[model.organizationId] ?? null)),
|
||||
this.keyService.orgKeys$(userId).pipe(
|
||||
map((orgKeys) => {
|
||||
if (!orgKeys) {
|
||||
throw new Error("No keys for the provided userId.");
|
||||
}
|
||||
|
||||
const key = orgKeys[model.organizationId];
|
||||
|
||||
if (key == null) {
|
||||
throw new Error("No key for this collection's organization.");
|
||||
}
|
||||
|
||||
return key;
|
||||
}),
|
||||
),
|
||||
);
|
||||
if (key == null) {
|
||||
throw new Error("No key for this collection's organization.");
|
||||
}
|
||||
const collection = new CollectionRequest();
|
||||
collection.externalId = model.externalId;
|
||||
collection.name = (await this.encryptService.encryptString(model.name, key)).encryptedString;
|
||||
collection.groups = model.groups.map(
|
||||
|
||||
const groups = model.groups.map(
|
||||
(group) =>
|
||||
new SelectionReadOnlyRequest(group.id, group.readOnly, group.hidePasswords, group.manage),
|
||||
);
|
||||
collection.users = model.users.map(
|
||||
|
||||
const users = model.users.map(
|
||||
(user) =>
|
||||
new SelectionReadOnlyRequest(user.id, user.readOnly, user.hidePasswords, user.manage),
|
||||
);
|
||||
return collection;
|
||||
|
||||
const collectionRequest = new CollectionRequest({
|
||||
name: await this.encryptService.encryptString(model.name, key),
|
||||
externalId: model.externalId,
|
||||
users,
|
||||
groups,
|
||||
});
|
||||
|
||||
return collectionRequest;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -390,9 +390,11 @@ const collectionDataFactory = (orgId?: OrganizationId) => {
|
||||
};
|
||||
|
||||
function collectionViewDataFactory(orgId?: OrganizationId): CollectionView {
|
||||
const collectionView = new CollectionView();
|
||||
collectionView.id = Utils.newGuid() as CollectionId;
|
||||
collectionView.organizationId = orgId ?? (Utils.newGuid() as OrganizationId);
|
||||
collectionView.name = "DEC_NAME_" + collectionView.id;
|
||||
const id = Utils.newGuid() as CollectionId;
|
||||
const collectionView = new CollectionView({
|
||||
id,
|
||||
organizationId: orgId ?? (Utils.newGuid() as OrganizationId),
|
||||
name: "DEC_NAME_" + id,
|
||||
});
|
||||
return collectionView;
|
||||
}
|
||||
|
||||
@@ -42,9 +42,7 @@ export class DefaultCollectionService implements CollectionService {
|
||||
/**
|
||||
* @returns a SingleUserState for encrypted collection data.
|
||||
*/
|
||||
private encryptedState(
|
||||
userId: UserId,
|
||||
): SingleUserState<Record<CollectionId, CollectionData | null>> {
|
||||
private encryptedState(userId: UserId): SingleUserState<Record<CollectionId, CollectionData>> {
|
||||
return this.stateProvider.getUser(userId, ENCRYPTED_COLLECTION_DATA_KEY);
|
||||
}
|
||||
|
||||
@@ -62,7 +60,7 @@ export class DefaultCollectionService implements CollectionService {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Object.values(collections).map((c) => new Collection(c));
|
||||
return Object.values(collections).map((c) => Collection.fromCollectionData(c));
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -110,8 +108,8 @@ export class DefaultCollectionService implements CollectionService {
|
||||
if (collections == null) {
|
||||
collections = {};
|
||||
}
|
||||
collections[toUpdate.id] = toUpdate;
|
||||
|
||||
collections[toUpdate.id] = toUpdate;
|
||||
return collections;
|
||||
});
|
||||
|
||||
@@ -121,7 +119,7 @@ export class DefaultCollectionService implements CollectionService {
|
||||
if (!orgKeys) {
|
||||
throw new Error("No key for this collection's organization.");
|
||||
}
|
||||
return this.decryptMany$([new Collection(toUpdate)], orgKeys);
|
||||
return this.decryptMany$([Collection.fromCollectionData(toUpdate)], orgKeys);
|
||||
}),
|
||||
),
|
||||
);
|
||||
@@ -177,10 +175,6 @@ export class DefaultCollectionService implements CollectionService {
|
||||
}
|
||||
|
||||
async encrypt(model: CollectionView, userId: UserId): Promise<Collection> {
|
||||
if (model.organizationId == null) {
|
||||
throw new Error("Collection has no organization id.");
|
||||
}
|
||||
|
||||
const key = await firstValueFrom(
|
||||
this.keyService.orgKeys$(userId).pipe(
|
||||
filter((orgKeys) => !!orgKeys),
|
||||
@@ -188,13 +182,7 @@ export class DefaultCollectionService implements CollectionService {
|
||||
),
|
||||
);
|
||||
|
||||
const collection = new Collection();
|
||||
collection.id = model.id;
|
||||
collection.organizationId = model.organizationId;
|
||||
collection.readOnly = model.readOnly;
|
||||
collection.externalId = model.externalId;
|
||||
collection.name = await this.encryptService.encryptString(model.name, key);
|
||||
return collection;
|
||||
return await model.encrypt(key, this.encryptService);
|
||||
}
|
||||
|
||||
// TODO: this should be private.
|
||||
@@ -211,7 +199,12 @@ export class DefaultCollectionService implements CollectionService {
|
||||
|
||||
collections.forEach((collection) => {
|
||||
decCollections.push(
|
||||
from(collection.decrypt(orgKeys[collection.organizationId as OrganizationId])),
|
||||
from(
|
||||
collection.decrypt(
|
||||
orgKeys[collection.organizationId as OrganizationId],
|
||||
this.encryptService,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -223,9 +216,8 @@ export class DefaultCollectionService implements CollectionService {
|
||||
getAllNested(collections: CollectionView[]): TreeNode<CollectionView>[] {
|
||||
const nodes: TreeNode<CollectionView>[] = [];
|
||||
collections.forEach((c) => {
|
||||
const collectionCopy = new CollectionView();
|
||||
collectionCopy.id = c.id;
|
||||
collectionCopy.organizationId = c.organizationId;
|
||||
const collectionCopy = Object.assign(new CollectionView({ ...c }), c);
|
||||
|
||||
const parts = c.name != null ? c.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : [];
|
||||
ServiceUtils.nestedTraverse(nodes, 0, parts, collectionCopy, undefined, NestingDelimiter);
|
||||
});
|
||||
|
||||
@@ -153,7 +153,8 @@ export class ManageTaxInformationComponent implements OnInit, OnDestroy, OnChang
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
// Clear the value of the tax-id if states have been changed in the parent component
|
||||
if (!changes.showTaxIdField.currentValue) {
|
||||
const showTaxIdField = changes["showTaxIdField"];
|
||||
if (showTaxIdField && !showTaxIdField.currentValue) {
|
||||
this.formGroup.controls.taxId.setValue(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
@@ -31,10 +30,6 @@ describe("Vault Nudges Service", () => {
|
||||
let fakeStateProvider: FakeStateProvider;
|
||||
|
||||
let testBed: TestBed;
|
||||
const mockConfigService = {
|
||||
getFeatureFlag$: jest.fn().mockReturnValue(of(true)),
|
||||
getFeatureFlag: jest.fn().mockReturnValue(true),
|
||||
};
|
||||
|
||||
const nudgeServices = [
|
||||
EmptyVaultNudgeService,
|
||||
@@ -58,7 +53,6 @@ describe("Vault Nudges Service", () => {
|
||||
provide: StateProvider,
|
||||
useValue: fakeStateProvider,
|
||||
},
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
{
|
||||
provide: HasItemsNudgeService,
|
||||
useValue: mock<HasItemsNudgeService>(),
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { inject, Injectable } from "@angular/core";
|
||||
import { combineLatest, map, Observable, of, shareReplay, switchMap } from "rxjs";
|
||||
import { combineLatest, map, Observable, shareReplay } from "rxjs";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { UserKeyDefinition, NUDGES_DISK } from "@bitwarden/common/platform/state";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values";
|
||||
@@ -83,7 +81,6 @@ export class NudgesService {
|
||||
* @private
|
||||
*/
|
||||
private defaultNudgeService = inject(DefaultSingleNudgeService);
|
||||
private configService = inject(ConfigService);
|
||||
|
||||
private getNudgeService(nudge: NudgeType): SingleNudgeService {
|
||||
return this.customNudgeServices[nudge] ?? this.defaultNudgeService;
|
||||
@@ -95,16 +92,9 @@ export class NudgesService {
|
||||
* @param userId
|
||||
*/
|
||||
showNudgeSpotlight$(nudge: NudgeType, userId: UserId): Observable<boolean> {
|
||||
return this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge).pipe(
|
||||
switchMap((hasVaultNudgeFlag) => {
|
||||
if (!hasVaultNudgeFlag) {
|
||||
return of(false);
|
||||
}
|
||||
return this.getNudgeService(nudge)
|
||||
.nudgeStatus$(nudge, userId)
|
||||
.pipe(map((nudgeStatus) => !nudgeStatus.hasSpotlightDismissed));
|
||||
}),
|
||||
);
|
||||
return this.getNudgeService(nudge)
|
||||
.nudgeStatus$(nudge, userId)
|
||||
.pipe(map((nudgeStatus) => !nudgeStatus.hasSpotlightDismissed));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,16 +103,9 @@ export class NudgesService {
|
||||
* @param userId
|
||||
*/
|
||||
showNudgeBadge$(nudge: NudgeType, userId: UserId): Observable<boolean> {
|
||||
return this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge).pipe(
|
||||
switchMap((hasVaultNudgeFlag) => {
|
||||
if (!hasVaultNudgeFlag) {
|
||||
return of(false);
|
||||
}
|
||||
return this.getNudgeService(nudge)
|
||||
.nudgeStatus$(nudge, userId)
|
||||
.pipe(map((nudgeStatus) => !nudgeStatus.hasBadgeDismissed));
|
||||
}),
|
||||
);
|
||||
return this.getNudgeService(nudge)
|
||||
.nudgeStatus$(nudge, userId)
|
||||
.pipe(map((nudgeStatus) => !nudgeStatus.hasBadgeDismissed));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,14 +114,7 @@ export class NudgesService {
|
||||
* @param userId
|
||||
*/
|
||||
showNudgeStatus$(nudge: NudgeType, userId: UserId) {
|
||||
return this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge).pipe(
|
||||
switchMap((hasVaultNudgeFlag) => {
|
||||
if (!hasVaultNudgeFlag) {
|
||||
return of({ hasBadgeDismissed: true, hasSpotlightDismissed: true } as NudgeStatus);
|
||||
}
|
||||
return this.getNudgeService(nudge).nudgeStatus$(nudge, userId);
|
||||
}),
|
||||
);
|
||||
return this.getNudgeService(nudge).nudgeStatus$(nudge, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -47,7 +47,6 @@ export enum FeatureFlag {
|
||||
EventBasedOrganizationIntegrations = "event-based-organization-integrations",
|
||||
|
||||
/* Vault */
|
||||
PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge",
|
||||
PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk",
|
||||
PM22134SdkCipherListView = "pm-22134-sdk-cipher-list-view",
|
||||
PM22136_SdkCipherEncryption = "pm-22136-sdk-cipher-encryption",
|
||||
@@ -92,7 +91,6 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.EventBasedOrganizationIntegrations]: FALSE,
|
||||
|
||||
/* Vault */
|
||||
[FeatureFlag.PM8851_BrowserOnboardingNudge]: FALSE,
|
||||
[FeatureFlag.CipherKeyEncryption]: FALSE,
|
||||
[FeatureFlag.PM19941MigrateCipherDomainToSdk]: FALSE,
|
||||
[FeatureFlag.RemoveCardItemTypePolicy]: FALSE,
|
||||
|
||||
@@ -3,20 +3,18 @@
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Collection as CollectionDomain, CollectionView } from "@bitwarden/admin-console/common";
|
||||
|
||||
import { CollectionId } from "../../types/guid";
|
||||
import { CollectionId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { CollectionExport } from "./collection.export";
|
||||
|
||||
export class CollectionWithIdExport extends CollectionExport {
|
||||
id: CollectionId;
|
||||
|
||||
static toView(req: CollectionWithIdExport, view = new CollectionView()) {
|
||||
view.id = req.id;
|
||||
return super.toView(req, view);
|
||||
static toView(req: CollectionWithIdExport) {
|
||||
return super.toView(req, req.id);
|
||||
}
|
||||
|
||||
static toDomain(req: CollectionWithIdExport, domain = new CollectionDomain()) {
|
||||
static toDomain(req: CollectionWithIdExport, domain: CollectionDomain) {
|
||||
domain.id = req.id;
|
||||
return super.toDomain(req, domain);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { Collection as CollectionDomain, CollectionView } from "@bitwarden/admin-console/common";
|
||||
|
||||
import { EncString } from "../../key-management/crypto/models/enc-string";
|
||||
import { emptyGuid, OrganizationId } from "../../types/guid";
|
||||
import { CollectionId, emptyGuid, OrganizationId } from "../../types/guid";
|
||||
|
||||
import { safeGetString } from "./utils";
|
||||
|
||||
@@ -18,16 +18,17 @@ export class CollectionExport {
|
||||
return req;
|
||||
}
|
||||
|
||||
static toView(req: CollectionExport, view = new CollectionView()) {
|
||||
view.name = req.name;
|
||||
static toView(req: CollectionExport, id: CollectionId) {
|
||||
const view = new CollectionView({
|
||||
name: req.name,
|
||||
organizationId: req.organizationId,
|
||||
id,
|
||||
});
|
||||
view.externalId = req.externalId;
|
||||
if (view.organizationId == null) {
|
||||
view.organizationId = req.organizationId;
|
||||
}
|
||||
return view;
|
||||
}
|
||||
|
||||
static toDomain(req: CollectionExport, domain = new CollectionDomain()) {
|
||||
static toDomain(req: CollectionExport, domain: CollectionDomain) {
|
||||
domain.name = req.name != null ? new EncString(req.name) : null;
|
||||
domain.externalId = req.externalId;
|
||||
if (domain.organizationId == null) {
|
||||
|
||||
@@ -13,8 +13,8 @@ export const getById = <TId, T extends { id: TId }>(id: TId) =>
|
||||
* @param id The IDs of the objects to return.
|
||||
* @returns An array containing objects with matching IDs, or an empty array if there are no matching objects.
|
||||
*/
|
||||
export const getByIds = <TId, T extends { id: TId | undefined }>(ids: TId[]) => {
|
||||
const idSet = new Set(ids.filter((id) => id != null));
|
||||
export const getByIds = <TId, T extends { id: TId }>(ids: TId[]) => {
|
||||
const idSet = new Set(ids);
|
||||
return map<T[], T[]>((objects) => {
|
||||
return objects.filter((o) => o.id && idSet.has(o.id));
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ export type DecryptedObject<
|
||||
> = Record<TDecryptedKeys, string> & Omit<TEncryptedObject, TDecryptedKeys>;
|
||||
|
||||
// extracts shared keys from the domain and view types
|
||||
export type EncryptableKeys<D extends Domain, V extends View> = (keyof D &
|
||||
type EncryptableKeys<D extends Domain, V extends View> = (keyof D &
|
||||
ConditionalKeys<D, EncString | null>) &
|
||||
(keyof V & ConditionalKeys<V, string | null>);
|
||||
|
||||
|
||||
@@ -4,12 +4,12 @@ import * as papa from "papaparse";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { Collection, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { normalizeExpiryYearFormat } from "@bitwarden/common/autofill/utils";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { FieldType, SecureNoteType, CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FieldView } from "@bitwarden/common/vault/models/view/field.view";
|
||||
@@ -278,9 +278,12 @@ export abstract class BaseImporter {
|
||||
protected moveFoldersToCollections(result: ImportResult) {
|
||||
result.folderRelationships.forEach((r) => result.collectionRelationships.push(r));
|
||||
result.collections = result.folders.map((f) => {
|
||||
const collection = new CollectionView();
|
||||
collection.name = f.name;
|
||||
collection.id = (f.id as CollectionId) ?? undefined; // folder id may be null, which is not suitable for collections.
|
||||
const collection = new CollectionView({
|
||||
name: f.name,
|
||||
organizationId: this.organizationId,
|
||||
// FIXME: Folder.id may be null, this should be changed when refactoring Folders to be ts-strict
|
||||
id: Collection.isCollectionId(f.id) ? f.id : null,
|
||||
});
|
||||
return collection;
|
||||
});
|
||||
result.folderRelationships = [];
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
import { concatMap, firstValueFrom, map } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { Collection, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
@@ -206,11 +206,20 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer {
|
||||
for (const c of data.collections) {
|
||||
let collectionView: CollectionView;
|
||||
if (data.encrypted) {
|
||||
const collection = CollectionWithIdExport.toDomain(c);
|
||||
collection.organizationId = this.organizationId;
|
||||
collectionView = await firstValueFrom(this.keyService.activeUserOrgKeys$).then((orgKeys) =>
|
||||
collection.decrypt(orgKeys[c.organizationId as OrganizationId]),
|
||||
const collection = CollectionWithIdExport.toDomain(
|
||||
c,
|
||||
new Collection({
|
||||
id: c.id,
|
||||
name: new EncString(c.name),
|
||||
organizationId: this.organizationId,
|
||||
}),
|
||||
);
|
||||
const collection$ = this.keyService.activeUserOrgKeys$.pipe(
|
||||
// FIXME: replace type assertion with narrowing
|
||||
map((keys) => keys[c.organizationId as OrganizationId]),
|
||||
concatMap((key) => collection.decrypt(key, this.encryptService)),
|
||||
);
|
||||
collectionView = await firstValueFrom(collection$);
|
||||
} else {
|
||||
collectionView = CollectionWithIdExport.toView(c);
|
||||
collectionView.organizationId = null;
|
||||
|
||||
@@ -46,8 +46,11 @@ export class PadlockCsvImporter extends BaseImporter implements Importer {
|
||||
}
|
||||
|
||||
if (addCollection) {
|
||||
const collection = new CollectionView();
|
||||
collection.name = tag;
|
||||
// FIXME use a different model if ID is not required.
|
||||
// @ts-expect-error current functionality creates this view with no Id since its being imported.
|
||||
const collection = new CollectionView({
|
||||
name: tag,
|
||||
});
|
||||
result.collections.push(collection);
|
||||
}
|
||||
|
||||
|
||||
@@ -47,8 +47,11 @@ export class PasspackCsvImporter extends BaseImporter implements Importer {
|
||||
}
|
||||
|
||||
if (addCollection) {
|
||||
const collection = new CollectionView();
|
||||
collection.name = tag;
|
||||
// FIXME use a different model if ID is not required.
|
||||
// @ts-expect-error current functionality creates this view with no Id since its being imported.
|
||||
const collection = new CollectionView({
|
||||
name: tag,
|
||||
});
|
||||
result.collections.push(collection);
|
||||
}
|
||||
|
||||
|
||||
@@ -487,8 +487,11 @@ describe("Password Depot 17 Xml Importer", () => {
|
||||
it("should parse groups nodes into collections when importing into an organization", async () => {
|
||||
const importer = new PasswordDepot17XmlImporter();
|
||||
importer.organizationId = "someOrgId" as OrganizationId;
|
||||
const collection = new CollectionView();
|
||||
collection.name = "tempDB";
|
||||
const collection = new CollectionView({
|
||||
name: "tempDB",
|
||||
organizationId: importer.organizationId,
|
||||
id: null,
|
||||
});
|
||||
const actual = [collection];
|
||||
|
||||
const result = await importer.parse(PasswordTestData);
|
||||
|
||||
@@ -145,20 +145,29 @@ describe("ImportService", () => {
|
||||
);
|
||||
});
|
||||
|
||||
const mockImportTargetCollection = new CollectionView();
|
||||
mockImportTargetCollection.id = "myImportTarget" as CollectionId;
|
||||
mockImportTargetCollection.name = "myImportTarget";
|
||||
mockImportTargetCollection.organizationId = organizationId;
|
||||
const mockName = "myImportTarget";
|
||||
const mockId = "myImportTarget" as CollectionId;
|
||||
const mockImportTargetCollection = new CollectionView({
|
||||
name: mockName,
|
||||
id: mockId,
|
||||
organizationId,
|
||||
});
|
||||
|
||||
const mockCollection1 = new CollectionView();
|
||||
mockCollection1.id = "collection1" as CollectionId;
|
||||
mockCollection1.name = "collection1";
|
||||
mockCollection1.organizationId = organizationId;
|
||||
const mockName1 = "collection1";
|
||||
const mockId1 = "collection1" as CollectionId;
|
||||
const mockCollection1 = new CollectionView({
|
||||
name: mockName1,
|
||||
id: mockId1,
|
||||
organizationId,
|
||||
});
|
||||
|
||||
const mockCollection2 = new CollectionView();
|
||||
mockCollection2.id = "collection2" as CollectionId;
|
||||
mockCollection2.name = "collection2";
|
||||
mockCollection2.organizationId = organizationId;
|
||||
const mockName2 = "collection2";
|
||||
const mockId2 = "collection2" as CollectionId;
|
||||
const mockCollection2 = new CollectionView({
|
||||
name: mockName2,
|
||||
id: mockId2,
|
||||
organizationId,
|
||||
});
|
||||
|
||||
it("passing importTarget adds it to collections", async () => {
|
||||
await importService["setImportTarget"](
|
||||
|
||||
@@ -501,7 +501,7 @@ export class ImportService implements ImportServiceAbstraction {
|
||||
const collections: CollectionView[] = [...importResult.collections];
|
||||
importResult.collections = [importTarget as CollectionView];
|
||||
collections.map((x) => {
|
||||
const f = new CollectionView();
|
||||
const f = new CollectionView(x);
|
||||
f.name = `${importTarget.name}/${x.name}`;
|
||||
importResult.collections.push(f);
|
||||
});
|
||||
|
||||
@@ -143,10 +143,14 @@ export class OrganizationVaultExportService
|
||||
if (exportData != null) {
|
||||
if (exportData.collections != null && exportData.collections.length > 0) {
|
||||
exportData.collections.forEach((c) => {
|
||||
const collection = new Collection(new CollectionData(c as CollectionDetailsResponse));
|
||||
const collection = Collection.fromCollectionData(
|
||||
new CollectionData(c as CollectionDetailsResponse),
|
||||
);
|
||||
exportPromises.push(
|
||||
firstValueFrom(this.keyService.activeUserOrgKeys$)
|
||||
.then((keys) => collection.decrypt(keys[organizationId as OrganizationId]))
|
||||
.then((keys) =>
|
||||
collection.decrypt(keys[organizationId as OrganizationId], this.encryptService),
|
||||
)
|
||||
.then((decCol) => {
|
||||
decCollections.push(decCol);
|
||||
}),
|
||||
@@ -191,7 +195,9 @@ export class OrganizationVaultExportService
|
||||
this.apiService.getCollections(organizationId).then((c) => {
|
||||
if (c != null && c.data != null && c.data.length > 0) {
|
||||
c.data.forEach((r) => {
|
||||
const collection = new Collection(new CollectionData(r as CollectionDetailsResponse));
|
||||
const collection = Collection.fromCollectionData(
|
||||
new CollectionData(r as CollectionDetailsResponse),
|
||||
);
|
||||
collections.push(collection);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ const createMockCollection = (
|
||||
canEdit: jest.fn(),
|
||||
canDelete: jest.fn(),
|
||||
canViewCollectionInfo: jest.fn(),
|
||||
encrypt: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -29,23 +29,27 @@ describe("AssignCollectionsComponent", () => {
|
||||
const mockUserId = "mock-user-id" as UserId;
|
||||
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
|
||||
|
||||
const editCollection = new CollectionView();
|
||||
editCollection.id = "collection-id" as CollectionId;
|
||||
editCollection.organizationId = "org-id" as OrganizationId;
|
||||
editCollection.name = "Editable Collection";
|
||||
const editCollection = new CollectionView({
|
||||
id: "collection-id" as CollectionId,
|
||||
organizationId: "org-id" as OrganizationId,
|
||||
name: "Editable Collection",
|
||||
});
|
||||
|
||||
editCollection.readOnly = false;
|
||||
editCollection.manage = true;
|
||||
|
||||
const readOnlyCollection1 = new CollectionView();
|
||||
readOnlyCollection1.id = "read-only-collection-id" as CollectionId;
|
||||
readOnlyCollection1.organizationId = "org-id" as OrganizationId;
|
||||
readOnlyCollection1.name = "Read Only Collection";
|
||||
const readOnlyCollection1 = new CollectionView({
|
||||
id: "read-only-collection-id" as CollectionId,
|
||||
organizationId: "org-id" as OrganizationId,
|
||||
name: "Read Only Collection",
|
||||
});
|
||||
readOnlyCollection1.readOnly = true;
|
||||
|
||||
const readOnlyCollection2 = new CollectionView();
|
||||
readOnlyCollection2.id = "read-only-collection-id-2" as CollectionId;
|
||||
readOnlyCollection2.organizationId = "org-id" as OrganizationId;
|
||||
readOnlyCollection2.name = "Read Only Collection 2";
|
||||
const readOnlyCollection2 = new CollectionView({
|
||||
id: "read-only-collection-id-2" as CollectionId,
|
||||
organizationId: "org-id" as OrganizationId,
|
||||
name: "Read Only Collection 2",
|
||||
});
|
||||
readOnlyCollection2.readOnly = true;
|
||||
|
||||
const params = {
|
||||
|
||||
Reference in New Issue
Block a user