mirror of
https://github.com/bitwarden/browser.git
synced 2025-03-02 03:41:09 +01:00
Merge remote-tracking branch 'origin/main' into pm-13115
This commit is contained in:
commit
f408574322
2
.github/workflows/build-desktop.yml
vendored
2
.github/workflows/build-desktop.yml
vendored
@ -1193,7 +1193,7 @@ jobs:
|
||||
if: |
|
||||
github.event_name != 'pull_request_target'
|
||||
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc-desktop')
|
||||
uses: slackapi/slack-github-action@fcfb566f8b0aab22203f066d80ca1d7e4b5d05b3 # v1.27.1
|
||||
uses: slackapi/slack-github-action@485a9d42d3a73031f12ec201c457e2162c45d02d # v2.0.0
|
||||
with:
|
||||
channel-id: C074F5UESQ0
|
||||
payload: |
|
||||
|
@ -2105,7 +2105,7 @@
|
||||
"message": "Aikakatkaisutoiminnon vahvistus"
|
||||
},
|
||||
"autoFillAndSave": {
|
||||
"message": "Täytä automaattisesti ja tallenna"
|
||||
"message": "Automaattitäytä ja tallenna"
|
||||
},
|
||||
"fillAndSave": {
|
||||
"message": "Täytä ja tallenna"
|
||||
@ -3482,7 +3482,7 @@
|
||||
"description": "Aria label for the totp code displayed in the inline menu for autofill"
|
||||
},
|
||||
"totpSecondsSpanAria": {
|
||||
"message": "Time remaining before current TOTP expires",
|
||||
"message": "Aika jäljellä, ennen kuin nykyinen TOTP vanhenee",
|
||||
"description": "Aria label for the totp seconds displayed in the inline menu for autofill"
|
||||
},
|
||||
"fillCredentialsFor": {
|
||||
@ -4810,7 +4810,7 @@
|
||||
"message": "Määritä kaksivaiheinen kirjautuminen"
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage1": {
|
||||
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025."
|
||||
"message": "Bitwarden lähettää tilisi sähköpostiosoitteeseen koodin, jolla voit vahvistaa kirjautumiset uusista laitteista helmikuusta 2025 alkaen."
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage2": {
|
||||
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access."
|
||||
|
@ -20,16 +20,16 @@
|
||||
"message": "Crea account"
|
||||
},
|
||||
"newToBitwarden": {
|
||||
"message": "New to Bitwarden?"
|
||||
"message": "Nuovo in Bitwarden?"
|
||||
},
|
||||
"logInWithPasskey": {
|
||||
"message": "Log in with passkey"
|
||||
"message": "Accedi con passkey"
|
||||
},
|
||||
"useSingleSignOn": {
|
||||
"message": "Use single sign-on"
|
||||
"message": "Usa il Single Sign-On"
|
||||
},
|
||||
"welcomeBack": {
|
||||
"message": "Welcome back"
|
||||
"message": "Bentornato"
|
||||
},
|
||||
"setAStrongPassword": {
|
||||
"message": "Imposta una password robusta"
|
||||
@ -120,7 +120,7 @@
|
||||
"message": "Copia password"
|
||||
},
|
||||
"copyPassphrase": {
|
||||
"message": "Copy passphrase"
|
||||
"message": "Copia passphrase"
|
||||
},
|
||||
"copyNote": {
|
||||
"message": "Copia nota"
|
||||
@ -153,13 +153,13 @@
|
||||
"message": "Copia numero licenza"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "Copy private key"
|
||||
"message": "Copia chiave privata"
|
||||
},
|
||||
"copyPublicKey": {
|
||||
"message": "Copy public key"
|
||||
"message": "Copia chiave pubblica"
|
||||
},
|
||||
"copyFingerprint": {
|
||||
"message": "Copy fingerprint"
|
||||
"message": "Copia impronta"
|
||||
},
|
||||
"copyCustomField": {
|
||||
"message": "Copia $FIELD$",
|
||||
@ -177,7 +177,7 @@
|
||||
"message": "Copia note"
|
||||
},
|
||||
"fill": {
|
||||
"message": "Fill",
|
||||
"message": "Riempi",
|
||||
"description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible."
|
||||
},
|
||||
"autoFill": {
|
||||
@ -193,10 +193,10 @@
|
||||
"message": "Riempi automaticamente identità"
|
||||
},
|
||||
"fillVerificationCode": {
|
||||
"message": "Fill verification code"
|
||||
"message": "Riempi codice di verifica"
|
||||
},
|
||||
"fillVerificationCodeAria": {
|
||||
"message": "Fill Verification Code",
|
||||
"message": "Riempi Codice di Verifica",
|
||||
"description": "Aria label for the heading displayed the inline menu for totp code autofill"
|
||||
},
|
||||
"generatePasswordCopied": {
|
||||
@ -239,7 +239,7 @@
|
||||
"message": "Aggiungi elemento"
|
||||
},
|
||||
"accountEmail": {
|
||||
"message": "Account email"
|
||||
"message": "Email dell'account"
|
||||
},
|
||||
"requestHint": {
|
||||
"message": "Richiedi suggerimento"
|
||||
@ -443,7 +443,7 @@
|
||||
"message": "Genera password"
|
||||
},
|
||||
"generatePassphrase": {
|
||||
"message": "Generate passphrase"
|
||||
"message": "Genera passphrase"
|
||||
},
|
||||
"regeneratePassword": {
|
||||
"message": "Rigenera password"
|
||||
@ -530,7 +530,7 @@
|
||||
"description": "Label for the avoid ambiguous characters checkbox."
|
||||
},
|
||||
"generatorPolicyInEffect": {
|
||||
"message": "Enterprise policy requirements have been applied to your generator options.",
|
||||
"message": "I requisiti di politica aziendale sono stati applicati alle opzioni del generatore.",
|
||||
"description": "Indicates that a policy limits the credential generator screen."
|
||||
},
|
||||
"searchVault": {
|
||||
@ -576,7 +576,7 @@
|
||||
"message": "Note"
|
||||
},
|
||||
"privateNote": {
|
||||
"message": "Private note"
|
||||
"message": "Nota privata"
|
||||
},
|
||||
"note": {
|
||||
"message": "Nota"
|
||||
@ -600,7 +600,7 @@
|
||||
"message": "Avvia il sito web"
|
||||
},
|
||||
"launchWebsiteName": {
|
||||
"message": "Launch website $ITEMNAME$",
|
||||
"message": "Apri sito web $ITEMNAME$",
|
||||
"placeholders": {
|
||||
"itemname": {
|
||||
"content": "$1",
|
||||
@ -633,7 +633,7 @@
|
||||
"message": "Timeout della sessione"
|
||||
},
|
||||
"vaultTimeoutHeader": {
|
||||
"message": "Vault timeout"
|
||||
"message": "Timeout cassaforte"
|
||||
},
|
||||
"otherOptions": {
|
||||
"message": "Altre opzioni"
|
||||
@ -651,13 +651,13 @@
|
||||
"message": "La tua cassaforte è bloccata. Verifica la tua identità per continuare."
|
||||
},
|
||||
"yourVaultIsLockedV2": {
|
||||
"message": "Your vault is locked"
|
||||
"message": "Cassaforte bloccata"
|
||||
},
|
||||
"yourAccountIsLocked": {
|
||||
"message": "Your account is locked"
|
||||
"message": "Il tuo account è bloccato"
|
||||
},
|
||||
"or": {
|
||||
"message": "or"
|
||||
"message": "o"
|
||||
},
|
||||
"unlock": {
|
||||
"message": "Sblocca"
|
||||
@ -852,7 +852,7 @@
|
||||
"message": "Accedi"
|
||||
},
|
||||
"logInToBitwarden": {
|
||||
"message": "Log in to Bitwarden"
|
||||
"message": "Accedi a Bitwarden"
|
||||
},
|
||||
"restartRegistration": {
|
||||
"message": "Riprova la registrazione"
|
||||
@ -888,10 +888,10 @@
|
||||
"message": "La verifica in due passaggi rende il tuo account più sicuro richiedendoti di verificare il tuo login usando un altro dispositivo come una chiave di sicurezza, app di autenticazione, SMS, telefonata, o email. Può essere abilitata nella cassaforte web su bitwarden.com. Vuoi visitare il sito?"
|
||||
},
|
||||
"twoStepLoginConfirmationContent": {
|
||||
"message": "Make your account more secure by setting up two-step login in the Bitwarden web app."
|
||||
"message": "Rendi il tuo account più sicuro impostando l'autenticazione a due fattori nell'app web di Bitwarden."
|
||||
},
|
||||
"twoStepLoginConfirmationTitle": {
|
||||
"message": "Continue to web app?"
|
||||
"message": "Aprire web app?"
|
||||
},
|
||||
"editedFolder": {
|
||||
"message": "Cartella salvata"
|
||||
@ -1005,7 +1005,7 @@
|
||||
"message": "Mostra le identità nella sezione Scheda per riempirle automaticamente."
|
||||
},
|
||||
"clickToAutofillOnVault": {
|
||||
"message": "Click items to autofill on Vault view"
|
||||
"message": "Clicca gli oggetti da riempire dalla sezione Cassaforte"
|
||||
},
|
||||
"clearClipboard": {
|
||||
"message": "Cancella appunti",
|
||||
@ -1126,7 +1126,7 @@
|
||||
"description": "WARNING (should stay in capitalized letters if the language permits)"
|
||||
},
|
||||
"warningCapitalized": {
|
||||
"message": "Warning",
|
||||
"message": "Attenzione",
|
||||
"description": "Warning (should maintain locale-relevant capitalization)"
|
||||
},
|
||||
"confirmVaultExport": {
|
||||
@ -1206,7 +1206,7 @@
|
||||
"message": "File"
|
||||
},
|
||||
"fileToShare": {
|
||||
"message": "File to share"
|
||||
"message": "File da condividere"
|
||||
},
|
||||
"selectFile": {
|
||||
"message": "Seleziona un file"
|
||||
@ -1317,10 +1317,10 @@
|
||||
"message": "Inserisci il codice di verifica a 6 cifre dalla tua app di autenticazione."
|
||||
},
|
||||
"authenticationTimeout": {
|
||||
"message": "Authentication timeout"
|
||||
"message": "Timeout autenticazione"
|
||||
},
|
||||
"authenticationSessionTimedOut": {
|
||||
"message": "The authentication session timed out. Please restart the login process."
|
||||
"message": "La sessione di autenticazione è scaduta. Accedi di nuovo."
|
||||
},
|
||||
"enterVerificationCodeEmail": {
|
||||
"message": "Inserisci il codice di verifica a 6 cifre inviato a $EMAIL$.",
|
||||
@ -1440,7 +1440,7 @@
|
||||
"message": "URL del server"
|
||||
},
|
||||
"selfHostBaseUrl": {
|
||||
"message": "Self-host server URL",
|
||||
"message": "URL server autogestito",
|
||||
"description": "Label for field requesting a self-hosted integration service URL"
|
||||
},
|
||||
"apiUrl": {
|
||||
@ -1472,10 +1472,10 @@
|
||||
"message": "Mostra suggerimenti di riempimento automatico nei campi del modulo"
|
||||
},
|
||||
"showInlineMenuIdentitiesLabel": {
|
||||
"message": "Display identities as suggestions"
|
||||
"message": "Mostra identità come consigli"
|
||||
},
|
||||
"showInlineMenuCardsLabel": {
|
||||
"message": "Display cards as suggestions"
|
||||
"message": "Mostra carte come consigli"
|
||||
},
|
||||
"showInlineMenuOnIconSelectionLabel": {
|
||||
"message": "Mostra suggerimenti quando l'icona è selezionata"
|
||||
@ -1768,7 +1768,7 @@
|
||||
"message": "Identità"
|
||||
},
|
||||
"typeSshKey": {
|
||||
"message": "SSH key"
|
||||
"message": "Chiave SSH"
|
||||
},
|
||||
"newItemHeader": {
|
||||
"message": "Nuovo $TYPE$",
|
||||
@ -1801,13 +1801,13 @@
|
||||
"message": "Cronologia delle password"
|
||||
},
|
||||
"generatorHistory": {
|
||||
"message": "Generator history"
|
||||
"message": "Cronologia generatore"
|
||||
},
|
||||
"clearGeneratorHistoryTitle": {
|
||||
"message": "Clear generator history"
|
||||
"message": "Cancella cronologia generatore"
|
||||
},
|
||||
"cleargGeneratorHistoryDescription": {
|
||||
"message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?"
|
||||
"message": "Se continui, tutte le voci verranno eliminate definitivamente dalla cronologia del generatore. Vuoi continuare?"
|
||||
},
|
||||
"back": {
|
||||
"message": "Indietro"
|
||||
@ -1846,7 +1846,7 @@
|
||||
"message": "Note sicure"
|
||||
},
|
||||
"sshKeys": {
|
||||
"message": "SSH Keys"
|
||||
"message": "Chiavi SSH"
|
||||
},
|
||||
"clear": {
|
||||
"message": "Cancella",
|
||||
@ -1929,10 +1929,10 @@
|
||||
"message": "Cancella cronologia"
|
||||
},
|
||||
"nothingToShow": {
|
||||
"message": "Nothing to show"
|
||||
"message": "Niente da mostrare"
|
||||
},
|
||||
"nothingGeneratedRecently": {
|
||||
"message": "You haven't generated anything recently"
|
||||
"message": "Non hai generato niente di recente"
|
||||
},
|
||||
"remove": {
|
||||
"message": "Rimuovi"
|
||||
@ -1993,16 +1993,16 @@
|
||||
"message": "Sblocca con PIN"
|
||||
},
|
||||
"setYourPinTitle": {
|
||||
"message": "Set PIN"
|
||||
"message": "Imposta PIN"
|
||||
},
|
||||
"setYourPinButton": {
|
||||
"message": "Set PIN"
|
||||
"message": "Imposta PIN"
|
||||
},
|
||||
"setYourPinCode": {
|
||||
"message": "Imposta il tuo codice PIN per sbloccare Bitwarden. Le tue impostazioni PIN saranno resettate se esci completamente dall'app."
|
||||
},
|
||||
"setYourPinCode1": {
|
||||
"message": "Your PIN will be used to unlock Bitwarden instead of your master password. Your PIN will reset if you ever fully log out of Bitwarden."
|
||||
"message": "Il tuo PIN sarà usato per sbloccare Bitwarden invece della password principale. Il PIN sarà ripristinato se ti disconnetterai completamente da Bitwarden."
|
||||
},
|
||||
"pinRequired": {
|
||||
"message": "Codice PIN obbligatorio."
|
||||
@ -2017,7 +2017,7 @@
|
||||
"message": "Sblocca con i dati biometrici"
|
||||
},
|
||||
"unlockWithMasterPassword": {
|
||||
"message": "Unlock with master password"
|
||||
"message": "Sblocca con password principale"
|
||||
},
|
||||
"awaitDesktop": {
|
||||
"message": "In attesa di conferma dal desktop"
|
||||
@ -2029,7 +2029,7 @@
|
||||
"message": "Blocca con la password principale al riavvio del browser"
|
||||
},
|
||||
"lockWithMasterPassOnRestart1": {
|
||||
"message": "Require master password on browser restart"
|
||||
"message": "Richiedi password principale al riavvio del browser"
|
||||
},
|
||||
"selectOneCollection": {
|
||||
"message": "Devi selezionare almeno una raccolta."
|
||||
@ -2067,7 +2067,7 @@
|
||||
"message": "Azione timeout cassaforte"
|
||||
},
|
||||
"vaultTimeoutAction1": {
|
||||
"message": "Timeout action"
|
||||
"message": "Azione al timeout"
|
||||
},
|
||||
"lock": {
|
||||
"message": "Blocca",
|
||||
@ -2355,14 +2355,14 @@
|
||||
"message": "Modifiche del dominio escluso salvate"
|
||||
},
|
||||
"limitSendViews": {
|
||||
"message": "Limit views"
|
||||
"message": "Limita visualizzazioni"
|
||||
},
|
||||
"limitSendViewsHint": {
|
||||
"message": "No one can view this Send after the limit is reached.",
|
||||
"message": "Nessuno potrà vedere questo Send al raggiungimento del limite.",
|
||||
"description": "Displayed under the limit views field on Send"
|
||||
},
|
||||
"limitSendViewsCount": {
|
||||
"message": "$ACCESSCOUNT$ views left",
|
||||
"message": "$ACCESSCOUNT$ visualizzazioni rimaste",
|
||||
"description": "Displayed under the limit views field on Send",
|
||||
"placeholders": {
|
||||
"accessCount": {
|
||||
@ -2376,14 +2376,14 @@
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendDetails": {
|
||||
"message": "Send details",
|
||||
"message": "Dettagli Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendTypeText": {
|
||||
"message": "Testo"
|
||||
},
|
||||
"sendTypeTextToShare": {
|
||||
"message": "Text to share"
|
||||
"message": "Testo da condividere"
|
||||
},
|
||||
"sendTypeFile": {
|
||||
"message": "File"
|
||||
@ -2393,7 +2393,7 @@
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"hideTextByDefault": {
|
||||
"message": "Hide text by default"
|
||||
"message": "Nascondi testo come default"
|
||||
},
|
||||
"expired": {
|
||||
"message": "Scaduto"
|
||||
@ -2440,7 +2440,7 @@
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"deleteSendPermanentConfirmation": {
|
||||
"message": "Are you sure you want to permanently delete this Send?",
|
||||
"message": "Sicuro di voler eliminare definitivamente questo Send?",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"editSend": {
|
||||
@ -2451,7 +2451,7 @@
|
||||
"message": "Data di eliminazione"
|
||||
},
|
||||
"deletionDateDescV2": {
|
||||
"message": "The Send will be permanently deleted on this date.",
|
||||
"message": "Il Send sarà cancellato definitivamente in questa data.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"expirationDate": {
|
||||
@ -2473,7 +2473,7 @@
|
||||
"message": "Personalizzato"
|
||||
},
|
||||
"sendPasswordDescV3": {
|
||||
"message": "Add an optional password for recipients to access this Send.",
|
||||
"message": "Richiedi ai destinatari una password opzionale per aprire questo Send.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"createSend": {
|
||||
@ -2500,11 +2500,11 @@
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendExpiresInHoursSingle": {
|
||||
"message": "The Send will be available to anyone with the link for the next 1 hour.",
|
||||
"message": "Il Send sarà disponibile a chiunque con il link per la prossima ora.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendExpiresInHours": {
|
||||
"message": "The Send will be available to anyone with the link for the next $HOURS$ hours.",
|
||||
"message": "Il Send sarà disponibile a chiunque con il link per le prossime $HOURS$ ore.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
@ -2514,11 +2514,11 @@
|
||||
}
|
||||
},
|
||||
"sendExpiresInDaysSingle": {
|
||||
"message": "The Send will be available to anyone with the link for the next 1 day.",
|
||||
"message": "Il Send sarà disponibile a chiunque con il link per il prossimo giorno.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendExpiresInDays": {
|
||||
"message": "The Send will be available to anyone with the link for the next $DAYS$ days.",
|
||||
"message": "Il Send sarà disponibile a chiunque con il link per i prossimi $DAYS$ giorni.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.",
|
||||
"placeholders": {
|
||||
"days": {
|
||||
@ -2536,11 +2536,11 @@
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendFilePopoutDialogText": {
|
||||
"message": "Pop out extension?",
|
||||
"message": "Scollegare estensione?",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendFilePopoutDialogDesc": {
|
||||
"message": "To create a file Send, you need to pop out the extension to a new window.",
|
||||
"message": "Per creare un file Send, devi scollegare l'estensione in una nuova finestra.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendLinuxChromiumFileWarning": {
|
||||
@ -2553,7 +2553,7 @@
|
||||
"message": "Per scegliere un file usando Safari, apri una nuova finestra cliccando questo banner."
|
||||
},
|
||||
"popOut": {
|
||||
"message": "Pop out"
|
||||
"message": "Scollega"
|
||||
},
|
||||
"sendFileCalloutHeader": {
|
||||
"message": "Prima di iniziare"
|
||||
@ -2574,7 +2574,7 @@
|
||||
"message": "Si è verificato un errore durante il salvataggio delle date di eliminazione e scadenza."
|
||||
},
|
||||
"hideYourEmail": {
|
||||
"message": "Hide your email address from viewers."
|
||||
"message": "Nascondi il tuo indirizzo email ai visualizzatori."
|
||||
},
|
||||
"passwordPrompt": {
|
||||
"message": "Richiedi di inserire la password principale di nuovo per visualizzare questo elemento"
|
||||
@ -2631,7 +2631,7 @@
|
||||
"description": "Used as a card title description on the set password page to explain why the user is there"
|
||||
},
|
||||
"cardMetrics": {
|
||||
"message": "out of $TOTAL$",
|
||||
"message": "di $TOTAL$",
|
||||
"placeholders": {
|
||||
"total": {
|
||||
"content": "$1",
|
||||
@ -2650,7 +2650,7 @@
|
||||
"message": "Minuti"
|
||||
},
|
||||
"vaultTimeoutPolicyAffectingOptions": {
|
||||
"message": "Enterprise policy requirements have been applied to your timeout options"
|
||||
"message": "I requisiti di politica aziendale sono stati applicati alle opzioni di timeout"
|
||||
},
|
||||
"vaultTimeoutPolicyInEffect": {
|
||||
"message": "Le politiche della tua organizzazione hanno impostato il timeout massimo consentito della tua cassaforte su $HOURS$ ore e $MINUTES$ minuti.",
|
||||
@ -2666,7 +2666,7 @@
|
||||
}
|
||||
},
|
||||
"vaultTimeoutPolicyInEffect1": {
|
||||
"message": "$HOURS$ hour(s) and $MINUTES$ minute(s) maximum.",
|
||||
"message": "Al massimo $HOURS$ ora/e e $MINUTES$ minuto/i.",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
@ -2679,7 +2679,7 @@
|
||||
}
|
||||
},
|
||||
"vaultTimeoutPolicyMaximumError": {
|
||||
"message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum",
|
||||
"message": "Il timeout supera la restrizione impostata dalla tua organizzazione: massimo $HOURS$ ora/e e $MINUTES$ minuto/i",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
@ -2793,10 +2793,10 @@
|
||||
"message": "Genera nome utente"
|
||||
},
|
||||
"generateEmail": {
|
||||
"message": "Generate email"
|
||||
"message": "Genera email"
|
||||
},
|
||||
"spinboxBoundariesHint": {
|
||||
"message": "Value must be between $MIN$ and $MAX$.",
|
||||
"message": "Il valore deve essere compreso tra $MIN$ e $MAX$.",
|
||||
"description": "Explains spin box minimum and maximum values to the user",
|
||||
"placeholders": {
|
||||
"min": {
|
||||
@ -2810,7 +2810,7 @@
|
||||
}
|
||||
},
|
||||
"passwordLengthRecommendationHint": {
|
||||
"message": " Use $RECOMMENDED$ characters or more to generate a strong password.",
|
||||
"message": " Usa $RECOMMENDED$ caratteri o più per generare una password forte.",
|
||||
"description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
|
||||
"placeholders": {
|
||||
"recommended": {
|
||||
@ -2820,7 +2820,7 @@
|
||||
}
|
||||
},
|
||||
"passphraseNumWordsRecommendationHint": {
|
||||
"message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.",
|
||||
"message": " Usa $RECOMMENDED$ parole o più per generare una passphrase forte.",
|
||||
"description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
|
||||
"placeholders": {
|
||||
"recommended": {
|
||||
@ -2861,11 +2861,11 @@
|
||||
"message": "Genera un alias email con un servizio di inoltro esterno."
|
||||
},
|
||||
"forwarderDomainName": {
|
||||
"message": "Email domain",
|
||||
"message": "Dominio email",
|
||||
"description": "Labels the domain name email forwarder service option"
|
||||
},
|
||||
"forwarderDomainNameHint": {
|
||||
"message": "Choose a domain that is supported by the selected service",
|
||||
"message": "Scegli un dominio supportato dal servizio selezionato",
|
||||
"description": "Guidance provided for email forwarding services that support multiple email domains."
|
||||
},
|
||||
"forwarderError": {
|
||||
@ -3068,25 +3068,25 @@
|
||||
"message": "Invia notifica di nuovo"
|
||||
},
|
||||
"viewAllLogInOptions": {
|
||||
"message": "View all log in options"
|
||||
"message": "Visualizza tutte le opzioni di accesso"
|
||||
},
|
||||
"viewAllLoginOptionsV1": {
|
||||
"message": "View all log in options"
|
||||
"message": "Visualizza tutte le opzioni di accesso"
|
||||
},
|
||||
"notificationSentDevice": {
|
||||
"message": "Una notifica è stata inviata al tuo dispositivo."
|
||||
},
|
||||
"aNotificationWasSentToYourDevice": {
|
||||
"message": "A notification was sent to your device"
|
||||
"message": "Una notifica è stata inviata al tuo dispositivo"
|
||||
},
|
||||
"makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
|
||||
"message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
|
||||
"message": "Assicurati che il tuo account sia sbloccato e che la frase dell'impronta digitale corrisponda nell'altro dispositivo"
|
||||
},
|
||||
"youWillBeNotifiedOnceTheRequestIsApproved": {
|
||||
"message": "You will be notified once the request is approved"
|
||||
"message": "Sarai notificato una volta che la richiesta sarà approvata"
|
||||
},
|
||||
"needAnotherOptionV1": {
|
||||
"message": "Need another option?"
|
||||
"message": "Bisogno di un'altra opzione?"
|
||||
},
|
||||
"loginInitiated": {
|
||||
"message": "Accesso avviato"
|
||||
@ -3182,16 +3182,16 @@
|
||||
"message": "Si apre in una nuova finestra"
|
||||
},
|
||||
"rememberThisDeviceToMakeFutureLoginsSeamless": {
|
||||
"message": "Remember this device to make future logins seamless"
|
||||
"message": "Ricorda questo dispositivo per rendere immediati i futuri accessi"
|
||||
},
|
||||
"deviceApprovalRequired": {
|
||||
"message": "Approvazione del dispositivo obbligatoria. Seleziona un'opzione di approvazione:"
|
||||
},
|
||||
"deviceApprovalRequiredV2": {
|
||||
"message": "Device approval required"
|
||||
"message": "Approvazione dispositivo richiesta"
|
||||
},
|
||||
"selectAnApprovalOptionBelow": {
|
||||
"message": "Select an approval option below"
|
||||
"message": "Seleziona un'opzione di approvazione sotto"
|
||||
},
|
||||
"rememberThisDevice": {
|
||||
"message": "Ricorda questo dispositivo"
|
||||
@ -3267,7 +3267,7 @@
|
||||
"message": "Email utente mancante"
|
||||
},
|
||||
"activeUserEmailNotFoundLoggingYouOut": {
|
||||
"message": "Active user email not found. Logging you out."
|
||||
"message": "Email utente attiva non trovata. Logout in corso."
|
||||
},
|
||||
"deviceTrusted": {
|
||||
"message": "Dispositivo fidato"
|
||||
@ -3478,11 +3478,11 @@
|
||||
"description": "Screen reader text (aria-label) for unlock account button in overlay"
|
||||
},
|
||||
"totpCodeAria": {
|
||||
"message": "Time-based One-Time Password Verification Code",
|
||||
"message": "Codice di Verifica One-Time a tempo",
|
||||
"description": "Aria label for the totp code displayed in the inline menu for autofill"
|
||||
},
|
||||
"totpSecondsSpanAria": {
|
||||
"message": "Time remaining before current TOTP expires",
|
||||
"message": "Tempo rimasto prima che l'attuale TOTP scada",
|
||||
"description": "Aria label for the totp seconds displayed in the inline menu for autofill"
|
||||
},
|
||||
"fillCredentialsFor": {
|
||||
@ -3711,10 +3711,10 @@
|
||||
"message": "Passkey"
|
||||
},
|
||||
"accessing": {
|
||||
"message": "Accessing"
|
||||
"message": "Accedendo a"
|
||||
},
|
||||
"loggedInExclamation": {
|
||||
"message": "Logged in!"
|
||||
"message": "Accesso effettuato!"
|
||||
},
|
||||
"passkeyNotCopied": {
|
||||
"message": "La passkey non sarà copiata"
|
||||
@ -3741,7 +3741,7 @@
|
||||
"message": "Nessun login corrispondente per questo sito"
|
||||
},
|
||||
"searchSavePasskeyNewLogin": {
|
||||
"message": "Search or save passkey as new login"
|
||||
"message": "Cerca o salva la passkey come nuovo login"
|
||||
},
|
||||
"confirm": {
|
||||
"message": "Conferma"
|
||||
@ -4208,13 +4208,13 @@
|
||||
"message": "Filtri"
|
||||
},
|
||||
"filterVault": {
|
||||
"message": "Filter vault"
|
||||
"message": "Filtra cassaforte"
|
||||
},
|
||||
"filterApplied": {
|
||||
"message": "One filter applied"
|
||||
"message": "Un filtro applicato"
|
||||
},
|
||||
"filterAppliedPlural": {
|
||||
"message": "$COUNT$ filters applied",
|
||||
"message": "$COUNT$ filtri applicati",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
@ -4328,7 +4328,7 @@
|
||||
"message": "Abilita animazioni"
|
||||
},
|
||||
"showAnimations": {
|
||||
"message": "Show animations"
|
||||
"message": "Mostra animazioni"
|
||||
},
|
||||
"addAccount": {
|
||||
"message": "Aggiungi account"
|
||||
@ -4546,13 +4546,13 @@
|
||||
"message": "Posizione elemento"
|
||||
},
|
||||
"fileSend": {
|
||||
"message": "File Send"
|
||||
"message": "Send di File"
|
||||
},
|
||||
"fileSends": {
|
||||
"message": "Send File"
|
||||
},
|
||||
"textSend": {
|
||||
"message": "Text Send"
|
||||
"message": "Send di Testo"
|
||||
},
|
||||
"textSends": {
|
||||
"message": "Send Testo"
|
||||
@ -4570,7 +4570,7 @@
|
||||
"message": "Mostra il numero di suggerimenti di riempimento automatico sull'icona dell'estensione"
|
||||
},
|
||||
"showQuickCopyActions": {
|
||||
"message": "Show quick copy actions on Vault"
|
||||
"message": "Mostra azioni di copia rapida nella Cassaforte"
|
||||
},
|
||||
"systemDefault": {
|
||||
"message": "Predefinito del sistema"
|
||||
@ -4579,37 +4579,37 @@
|
||||
"message": "I requisiti della policy aziendale sono stati applicati a questa impostazione"
|
||||
},
|
||||
"sshPrivateKey": {
|
||||
"message": "Private key"
|
||||
"message": "Chiave privata"
|
||||
},
|
||||
"sshPublicKey": {
|
||||
"message": "Public key"
|
||||
"message": "Chiave pubblica"
|
||||
},
|
||||
"sshFingerprint": {
|
||||
"message": "Fingerprint"
|
||||
"message": "Impronta digitale"
|
||||
},
|
||||
"sshKeyAlgorithm": {
|
||||
"message": "Key type"
|
||||
"message": "Tipo di chiave"
|
||||
},
|
||||
"sshKeyAlgorithmED25519": {
|
||||
"message": "ED25519"
|
||||
},
|
||||
"sshKeyAlgorithmRSA2048": {
|
||||
"message": "RSA 2048-Bit"
|
||||
"message": "RSA a 2048 bit"
|
||||
},
|
||||
"sshKeyAlgorithmRSA3072": {
|
||||
"message": "RSA 3072-Bit"
|
||||
"message": "RSA a 3072 bit"
|
||||
},
|
||||
"sshKeyAlgorithmRSA4096": {
|
||||
"message": "RSA 4096-Bit"
|
||||
"message": "RSA a 4096 bit"
|
||||
},
|
||||
"retry": {
|
||||
"message": "Retry"
|
||||
"message": "Riprova"
|
||||
},
|
||||
"vaultCustomTimeoutMinimum": {
|
||||
"message": "Minimum custom timeout is 1 minute."
|
||||
"message": "Il timeout personalizzato minimo è 1 minuto."
|
||||
},
|
||||
"additionalContentAvailable": {
|
||||
"message": "Additional content is available"
|
||||
"message": "Sono disponibili ulteriori contenuti"
|
||||
},
|
||||
"fileSavedToDevice": {
|
||||
"message": "File salvato sul dispositivo. Gestisci dai download del dispositivo."
|
||||
@ -4642,22 +4642,22 @@
|
||||
"message": "Non hai i permessi per modificare questo elemento"
|
||||
},
|
||||
"authenticating": {
|
||||
"message": "Authenticating"
|
||||
"message": "Autenticazione"
|
||||
},
|
||||
"fillGeneratedPassword": {
|
||||
"message": "Fill generated password",
|
||||
"message": "Riempi password generata",
|
||||
"description": "Heading for the password generator within the inline menu"
|
||||
},
|
||||
"passwordRegenerated": {
|
||||
"message": "Password regenerated",
|
||||
"message": "Password rigenerata",
|
||||
"description": "Notification message for when a password has been regenerated"
|
||||
},
|
||||
"saveLoginToBitwarden": {
|
||||
"message": "Save login to Bitwarden?",
|
||||
"message": "Salvare il login su Bitwarden?",
|
||||
"description": "Confirmation message for saving a login to Bitwarden"
|
||||
},
|
||||
"spaceCharacterDescriptor": {
|
||||
"message": "Space",
|
||||
"message": "Spazio",
|
||||
"description": "Represents the space key in screen reader content as a readable word"
|
||||
},
|
||||
"tildeCharacterDescriptor": {
|
||||
@ -4669,157 +4669,157 @@
|
||||
"description": "Represents the ` key in screen reader content as a readable word"
|
||||
},
|
||||
"exclamationCharacterDescriptor": {
|
||||
"message": "Exclamation mark",
|
||||
"message": "Punto esclamativo",
|
||||
"description": "Represents the ! key in screen reader content as a readable word"
|
||||
},
|
||||
"atSignCharacterDescriptor": {
|
||||
"message": "At sign",
|
||||
"message": "Chiocciola",
|
||||
"description": "Represents the @ key in screen reader content as a readable word"
|
||||
},
|
||||
"hashSignCharacterDescriptor": {
|
||||
"message": "Hash sign",
|
||||
"message": "Cancelletto",
|
||||
"description": "Represents the # key in screen reader content as a readable word"
|
||||
},
|
||||
"dollarSignCharacterDescriptor": {
|
||||
"message": "Dollar sign",
|
||||
"message": "Simbolo del dollaro",
|
||||
"description": "Represents the $ key in screen reader content as a readable word"
|
||||
},
|
||||
"percentSignCharacterDescriptor": {
|
||||
"message": "Percent sign",
|
||||
"message": "Segno di percentuale",
|
||||
"description": "Represents the % key in screen reader content as a readable word"
|
||||
},
|
||||
"caretCharacterDescriptor": {
|
||||
"message": "Caret",
|
||||
"message": "Accento circonflesso",
|
||||
"description": "Represents the ^ key in screen reader content as a readable word"
|
||||
},
|
||||
"ampersandCharacterDescriptor": {
|
||||
"message": "Ampersand",
|
||||
"message": "E commerciale",
|
||||
"description": "Represents the & key in screen reader content as a readable word"
|
||||
},
|
||||
"asteriskCharacterDescriptor": {
|
||||
"message": "Asterisk",
|
||||
"message": "Asterisco",
|
||||
"description": "Represents the * key in screen reader content as a readable word"
|
||||
},
|
||||
"parenLeftCharacterDescriptor": {
|
||||
"message": "Left parenthesis",
|
||||
"message": "Parentesi sinistra",
|
||||
"description": "Represents the ( key in screen reader content as a readable word"
|
||||
},
|
||||
"parenRightCharacterDescriptor": {
|
||||
"message": "Right parenthesis",
|
||||
"message": "Parentesi destra",
|
||||
"description": "Represents the ) key in screen reader content as a readable word"
|
||||
},
|
||||
"hyphenCharacterDescriptor": {
|
||||
"message": "Underscore",
|
||||
"message": "Trattino basso",
|
||||
"description": "Represents the _ key in screen reader content as a readable word"
|
||||
},
|
||||
"underscoreCharacterDescriptor": {
|
||||
"message": "Hyphen",
|
||||
"message": "Trattino",
|
||||
"description": "Represents the - key in screen reader content as a readable word"
|
||||
},
|
||||
"plusCharacterDescriptor": {
|
||||
"message": "Plus",
|
||||
"message": "Più",
|
||||
"description": "Represents the + key in screen reader content as a readable word"
|
||||
},
|
||||
"equalsCharacterDescriptor": {
|
||||
"message": "Equals",
|
||||
"message": "Uguale",
|
||||
"description": "Represents the = key in screen reader content as a readable word"
|
||||
},
|
||||
"braceLeftCharacterDescriptor": {
|
||||
"message": "Left brace",
|
||||
"message": "Parentesi graffa aperta",
|
||||
"description": "Represents the { key in screen reader content as a readable word"
|
||||
},
|
||||
"braceRightCharacterDescriptor": {
|
||||
"message": "Right brace",
|
||||
"message": "Parentesi graffa chiusa",
|
||||
"description": "Represents the } key in screen reader content as a readable word"
|
||||
},
|
||||
"bracketLeftCharacterDescriptor": {
|
||||
"message": "Left bracket",
|
||||
"message": "Parentesi quadra aperta",
|
||||
"description": "Represents the [ key in screen reader content as a readable word"
|
||||
},
|
||||
"bracketRightCharacterDescriptor": {
|
||||
"message": "Right bracket",
|
||||
"message": "Parentesi quadra chiusa",
|
||||
"description": "Represents the ] key in screen reader content as a readable word"
|
||||
},
|
||||
"pipeCharacterDescriptor": {
|
||||
"message": "Pipe",
|
||||
"message": "Barra verticale",
|
||||
"description": "Represents the | key in screen reader content as a readable word"
|
||||
},
|
||||
"backSlashCharacterDescriptor": {
|
||||
"message": "Back slash",
|
||||
"message": "Barra rovesciata",
|
||||
"description": "Represents the back slash key in screen reader content as a readable word"
|
||||
},
|
||||
"colonCharacterDescriptor": {
|
||||
"message": "Colon",
|
||||
"message": "Due punti",
|
||||
"description": "Represents the : key in screen reader content as a readable word"
|
||||
},
|
||||
"semicolonCharacterDescriptor": {
|
||||
"message": "Semicolon",
|
||||
"message": "Punto e virgola",
|
||||
"description": "Represents the ; key in screen reader content as a readable word"
|
||||
},
|
||||
"doubleQuoteCharacterDescriptor": {
|
||||
"message": "Double quote",
|
||||
"message": "Doppi apici",
|
||||
"description": "Represents the double quote key in screen reader content as a readable word"
|
||||
},
|
||||
"singleQuoteCharacterDescriptor": {
|
||||
"message": "Single quote",
|
||||
"message": "Apostrofo",
|
||||
"description": "Represents the ' key in screen reader content as a readable word"
|
||||
},
|
||||
"lessThanCharacterDescriptor": {
|
||||
"message": "Less than",
|
||||
"message": "Minore",
|
||||
"description": "Represents the < key in screen reader content as a readable word"
|
||||
},
|
||||
"greaterThanCharacterDescriptor": {
|
||||
"message": "Greater than",
|
||||
"message": "Maggiore",
|
||||
"description": "Represents the > key in screen reader content as a readable word"
|
||||
},
|
||||
"commaCharacterDescriptor": {
|
||||
"message": "Comma",
|
||||
"message": "Virgola",
|
||||
"description": "Represents the , key in screen reader content as a readable word"
|
||||
},
|
||||
"periodCharacterDescriptor": {
|
||||
"message": "Period",
|
||||
"message": "Punto",
|
||||
"description": "Represents the . key in screen reader content as a readable word"
|
||||
},
|
||||
"questionCharacterDescriptor": {
|
||||
"message": "Question mark",
|
||||
"message": "Punto interrogativo",
|
||||
"description": "Represents the ? key in screen reader content as a readable word"
|
||||
},
|
||||
"forwardSlashCharacterDescriptor": {
|
||||
"message": "Forward slash",
|
||||
"message": "Slash",
|
||||
"description": "Represents the / key in screen reader content as a readable word"
|
||||
},
|
||||
"lowercaseAriaLabel": {
|
||||
"message": "Lowercase"
|
||||
"message": "Minuscolo"
|
||||
},
|
||||
"uppercaseAriaLabel": {
|
||||
"message": "Uppercase"
|
||||
"message": "Maiuscolo"
|
||||
},
|
||||
"generatedPassword": {
|
||||
"message": "Generated password"
|
||||
"message": "Password generata"
|
||||
},
|
||||
"compactMode": {
|
||||
"message": "Compact mode"
|
||||
"message": "Modalità compatta"
|
||||
},
|
||||
"beta": {
|
||||
"message": "Beta"
|
||||
},
|
||||
"importantNotice": {
|
||||
"message": "Important notice"
|
||||
"message": "Avviso importante"
|
||||
},
|
||||
"setupTwoStepLogin": {
|
||||
"message": "Set up two-step login"
|
||||
"message": "Imposta accesso in due passaggi"
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage1": {
|
||||
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025."
|
||||
"message": "Bitwarden invierà un codice all'email del tuo account per verificare gli accessi da nuovi dispositivi a partire da febbraio 2025."
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage2": {
|
||||
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access."
|
||||
"message": "Puoi impostare l'accesso in due passaggi come modo alternativo per proteggere il tuo account, o cambiare la tua e-mail in una alla quale puoi accedere."
|
||||
},
|
||||
"remindMeLater": {
|
||||
"message": "Remind me later"
|
||||
"message": "Ricordamelo più tardi"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneFormContent": {
|
||||
"message": "Do you have reliable access to your email, $EMAIL$?",
|
||||
"message": "Riesci ancora ad accedere a questa email, $EMAIL$?",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
@ -4828,24 +4828,24 @@
|
||||
}
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessNo": {
|
||||
"message": "No, I do not"
|
||||
"message": "No, non riesco"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessYes": {
|
||||
"message": "Yes, I can reliably access my email"
|
||||
"message": "Sì, riesco ad accedere a questa email"
|
||||
},
|
||||
"turnOnTwoStepLogin": {
|
||||
"message": "Turn on two-step login"
|
||||
"message": "Attiva accesso in due passaggi"
|
||||
},
|
||||
"changeAcctEmail": {
|
||||
"message": "Change account email"
|
||||
"message": "Cambia l'email dell'account"
|
||||
},
|
||||
"extensionWidth": {
|
||||
"message": "Extension width"
|
||||
"message": "Larghezza estensione"
|
||||
},
|
||||
"wide": {
|
||||
"message": "Wide"
|
||||
"message": "Larga"
|
||||
},
|
||||
"extraWide": {
|
||||
"message": "Extra wide"
|
||||
"message": "Molto larga"
|
||||
}
|
||||
}
|
||||
|
@ -1005,7 +1005,7 @@
|
||||
"message": "自動入力を簡単にするために、タブページに ID アイテムを表示します"
|
||||
},
|
||||
"clickToAutofillOnVault": {
|
||||
"message": "Click items to autofill on Vault view"
|
||||
"message": "保管庫で、自動入力するアイテムをクリックしてください"
|
||||
},
|
||||
"clearClipboard": {
|
||||
"message": "クリップボードの消去",
|
||||
@ -4810,10 +4810,10 @@
|
||||
"message": "2段階認証を設定する"
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage1": {
|
||||
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025."
|
||||
"message": "Bitwarden は2025年2月以降、新しいデバイスからのログイン時にアカウントのメールアドレスに確認コードを送信します。"
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage2": {
|
||||
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access."
|
||||
"message": "代わりに2段階認証によるログインでアカウントを保護するか、メールアドレスをあなたがアクセスできるものに変更できます。"
|
||||
},
|
||||
"remindMeLater": {
|
||||
"message": "後で再通知"
|
||||
@ -4831,13 +4831,13 @@
|
||||
"message": "いいえ、違います。"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessYes": {
|
||||
"message": "Yes, I can reliably access my email"
|
||||
"message": "はい、メールアドレスには私が確実にアクセスできます"
|
||||
},
|
||||
"turnOnTwoStepLogin": {
|
||||
"message": "Turn on two-step login"
|
||||
"message": "2段階認証によるログインを有効にする"
|
||||
},
|
||||
"changeAcctEmail": {
|
||||
"message": "Change account email"
|
||||
"message": "アカウントのメールアドレスを変更する"
|
||||
},
|
||||
"extensionWidth": {
|
||||
"message": "拡張機能の幅"
|
||||
|
@ -4154,7 +4154,7 @@
|
||||
"message": "Ek bilgiler"
|
||||
},
|
||||
"itemHistory": {
|
||||
"message": "Öğe geçmişi"
|
||||
"message": "Kayıt geçmişi"
|
||||
},
|
||||
"lastEdited": {
|
||||
"message": "Son düzenlenme"
|
||||
|
@ -428,7 +428,7 @@
|
||||
"description": "Short for 'credential generator'."
|
||||
},
|
||||
"passGenInfo": {
|
||||
"message": "自动生成安全可靠唯一的登录密码。"
|
||||
"message": "自动为您的登录生成强大且唯一的密码。"
|
||||
},
|
||||
"bitWebVaultApp": {
|
||||
"message": "Bitwarden 网页 App"
|
||||
@ -888,7 +888,7 @@
|
||||
"message": "两步登录要求您从其他设备(例如安全钥匙、验证器 App、短信、电话或者电子邮件)来验证您的登录,这能使您的账户更加安全。两步登录需要在 bitwarden.com 网页版密码库中设置。现在访问此网站吗?"
|
||||
},
|
||||
"twoStepLoginConfirmationContent": {
|
||||
"message": "通过在 Bitwarden 网页 App 中设置两步登录,可以使您的账户更加安全。"
|
||||
"message": "在 Bitwarden 网页 App 中设置两步登录,让您的账户更加安全。"
|
||||
},
|
||||
"twoStepLoginConfirmationTitle": {
|
||||
"message": "前往网页 App 吗?"
|
||||
@ -2123,7 +2123,7 @@
|
||||
"message": "您仍然想要填充此登录信息吗?"
|
||||
},
|
||||
"autofillIframeWarning": {
|
||||
"message": "该表单由不同于您保存的登录的 URI 域名托管。选择「确定」以自动填充,或选择「取消」停止填充。"
|
||||
"message": "该表单由与您保存的登录 URI 不同的域名托管。选择「确定」继续自动填充,或选择「取消」停止自动填充。"
|
||||
},
|
||||
"autofillIframeWarningTip": {
|
||||
"message": "要防止以后出现此警告,请将此站点的 URI $HOSTNAME$ 保存到您的 Bitwarden 登录项目中。",
|
||||
@ -3982,7 +3982,7 @@
|
||||
"message": "自动填充建议"
|
||||
},
|
||||
"autofillSuggestionsTip": {
|
||||
"message": "保存此站点的登录项目用来自动填充"
|
||||
"message": "将此站点保存为登录项目以用于自动填充"
|
||||
},
|
||||
"yourVaultIsEmpty": {
|
||||
"message": "您的密码库是空的"
|
||||
|
@ -1,140 +0,0 @@
|
||||
<app-header>
|
||||
<div class="left">
|
||||
<button type="button" routerLink="/tabs/settings">
|
||||
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
|
||||
<span>{{ "back" | i18n }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<h1 class="center">
|
||||
<span class="title">{{ "accountSecurity" | i18n }}</span>
|
||||
</h1>
|
||||
<div class="right">
|
||||
<app-pop-out></app-pop-out>
|
||||
</div>
|
||||
</app-header>
|
||||
<main tabindex="-1" [formGroup]="form">
|
||||
<div class="box list">
|
||||
<h2 class="box-header">{{ "unlockMethods" | i18n }}</h2>
|
||||
<div class="box-content single-line">
|
||||
<div class="box-content-row box-content-row-checkbox" appBoxRow *ngIf="supportsBiometric">
|
||||
<label for="biometric">{{ "unlockWithBiometrics" | i18n }}</label>
|
||||
<input id="biometric" type="checkbox" formControlName="biometric" />
|
||||
</div>
|
||||
<div
|
||||
class="box-content-row box-content-row-checkbox"
|
||||
appBoxRow
|
||||
*ngIf="supportsBiometric && this.form.value.biometric"
|
||||
>
|
||||
<label for="autoBiometricsPrompt">{{ "enableAutoBiometricsPrompt" | i18n }}</label>
|
||||
<input
|
||||
id="autoBiometricsPrompt"
|
||||
type="checkbox"
|
||||
(change)="updateAutoBiometricsPrompt()"
|
||||
formControlName="enableAutoBiometricsPrompt"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||
<label for="pin">{{ "unlockWithPin" | i18n }}</label>
|
||||
<input id="pin" type="checkbox" formControlName="pin" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box list">
|
||||
<h2 class="box-header">{{ "sessionTimeoutHeader" | i18n }}</h2>
|
||||
<div class="box-content single-line">
|
||||
<app-callout type="info" *ngIf="vaultTimeoutPolicyCallout | async as policy">
|
||||
<span *ngIf="policy.timeout && policy.action">
|
||||
{{
|
||||
"vaultTimeoutPolicyWithActionInEffect"
|
||||
| i18n: policy.timeout.hours : policy.timeout.minutes : (policy.action | i18n)
|
||||
}}
|
||||
</span>
|
||||
<span *ngIf="policy.timeout && !policy.action">
|
||||
{{ "vaultTimeoutPolicyInEffect" | i18n: policy.timeout.hours : policy.timeout.minutes }}
|
||||
</span>
|
||||
<span *ngIf="!policy.timeout && policy.action">
|
||||
{{ "vaultTimeoutActionPolicyInEffect" | i18n: (policy.action | i18n) }}
|
||||
</span>
|
||||
</app-callout>
|
||||
<app-vault-timeout-input
|
||||
[vaultTimeoutOptions]="vaultTimeoutOptions"
|
||||
[formControl]="form.controls.vaultTimeout"
|
||||
ngDefaultControl
|
||||
>
|
||||
</app-vault-timeout-input>
|
||||
<div class="box-content-row display-block" appBoxRow>
|
||||
<label for="vaultTimeoutAction">{{ "vaultTimeoutAction" | i18n }}</label>
|
||||
<select
|
||||
id="vaultTimeoutAction"
|
||||
name="VaultTimeoutActions"
|
||||
formControlName="vaultTimeoutAction"
|
||||
>
|
||||
<option *ngFor="let action of availableVaultTimeoutActions" [ngValue]="action">
|
||||
{{ action | i18n }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div
|
||||
*ngIf="!availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock)"
|
||||
id="unlockMethodHelp"
|
||||
class="box-footer"
|
||||
>
|
||||
{{ "unlockMethodNeededToChangeTimeoutActionDesc" | i18n }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box list">
|
||||
<h2 class="box-header">{{ "otherOptions" | i18n }}</h2>
|
||||
<div class="box-content single-line">
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row box-content-row-flex text-default"
|
||||
appStopClick
|
||||
(click)="fingerprint()"
|
||||
>
|
||||
<div class="row-main">{{ "fingerprintPhrase" | i18n }}</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row box-content-row-flex text-default"
|
||||
appStopClick
|
||||
(click)="twoStep()"
|
||||
>
|
||||
<div class="row-main">{{ "twoStepLogin" | i18n }}</div>
|
||||
<i class="bwi bwi-external-link bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row box-content-row-flex text-default"
|
||||
appStopClick
|
||||
(click)="changePassword()"
|
||||
*ngIf="showChangeMasterPass"
|
||||
>
|
||||
<div class="row-main">{{ "changeMasterPassword" | i18n }}</div>
|
||||
<i class="bwi bwi-external-link bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="
|
||||
!accountSwitcherEnabled && availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock)
|
||||
"
|
||||
type="button"
|
||||
class="box-content-row box-content-row-flex text-default"
|
||||
appStopClick
|
||||
(click)="lock()"
|
||||
>
|
||||
<div class="row-main">{{ "lockNow" | i18n }}</div>
|
||||
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="!accountSwitcherEnabled"
|
||||
type="button"
|
||||
class="box-content-row box-content-row-flex text-default"
|
||||
appStopClick
|
||||
(click)="logOut()"
|
||||
>
|
||||
<div class="row-main">{{ "logOut" | i18n }}</div>
|
||||
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
@ -1,499 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
concatMap,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
firstValueFrom,
|
||||
map,
|
||||
Observable,
|
||||
pairwise,
|
||||
startWith,
|
||||
Subject,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
} from "rxjs";
|
||||
|
||||
import { FingerprintDialogComponent } from "@bitwarden/auth/angular";
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import {
|
||||
VaultTimeout,
|
||||
VaultTimeoutOption,
|
||||
VaultTimeoutStringType,
|
||||
} from "@bitwarden/common/types/vault-timeout.type";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { KeyService, BiometricStateService, BiometricsService } from "@bitwarden/key-management";
|
||||
|
||||
import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors";
|
||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||
import { enableAccountSwitching } from "../../../platform/flags";
|
||||
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||
import { SetPinComponent } from "../components/set-pin.component";
|
||||
|
||||
import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component";
|
||||
|
||||
@Component({
|
||||
selector: "auth-account-security",
|
||||
templateUrl: "account-security-v1.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class AccountSecurityComponent implements OnInit, OnDestroy {
|
||||
protected readonly VaultTimeoutAction = VaultTimeoutAction;
|
||||
|
||||
availableVaultTimeoutActions: VaultTimeoutAction[] = [];
|
||||
vaultTimeoutOptions: VaultTimeoutOption[];
|
||||
vaultTimeoutPolicyCallout: Observable<{
|
||||
timeout: { hours: string; minutes: string };
|
||||
action: VaultTimeoutAction;
|
||||
}>;
|
||||
supportsBiometric: boolean;
|
||||
showChangeMasterPass = true;
|
||||
accountSwitcherEnabled = false;
|
||||
|
||||
form = this.formBuilder.group({
|
||||
vaultTimeout: [null as VaultTimeout | null],
|
||||
vaultTimeoutAction: [VaultTimeoutAction.Lock],
|
||||
pin: [null as boolean | null],
|
||||
biometric: false,
|
||||
enableAutoBiometricsPrompt: true,
|
||||
});
|
||||
|
||||
private refreshTimeoutSettings$ = new BehaviorSubject<void>(undefined);
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private pinService: PinServiceAbstraction,
|
||||
private policyService: PolicyService,
|
||||
private formBuilder: FormBuilder,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private vaultTimeoutService: VaultTimeoutService,
|
||||
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||
public messagingService: MessagingService,
|
||||
private environmentService: EnvironmentService,
|
||||
private keyService: KeyService,
|
||||
private stateService: StateService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
private dialogService: DialogService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private biometricStateService: BiometricStateService,
|
||||
private biometricsService: BiometricsService,
|
||||
) {
|
||||
this.accountSwitcherEnabled = enableAccountSwitching();
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
const maximumVaultTimeoutPolicy = this.policyService.get$(PolicyType.MaximumVaultTimeout);
|
||||
this.vaultTimeoutPolicyCallout = maximumVaultTimeoutPolicy.pipe(
|
||||
filter((policy) => policy != null),
|
||||
map((policy) => {
|
||||
let timeout;
|
||||
if (policy.data?.minutes) {
|
||||
timeout = {
|
||||
hours: Math.floor(policy.data?.minutes / 60).toString(),
|
||||
minutes: (policy.data?.minutes % 60).toString(),
|
||||
};
|
||||
}
|
||||
return { timeout: timeout, action: policy.data?.action };
|
||||
}),
|
||||
);
|
||||
|
||||
const showOnLocked =
|
||||
!this.platformUtilsService.isFirefox() && !this.platformUtilsService.isSafari();
|
||||
|
||||
this.vaultTimeoutOptions = [
|
||||
{ name: this.i18nService.t("immediately"), value: 0 },
|
||||
{ name: this.i18nService.t("oneMinute"), value: 1 },
|
||||
{ name: this.i18nService.t("fiveMinutes"), value: 5 },
|
||||
{ name: this.i18nService.t("fifteenMinutes"), value: 15 },
|
||||
{ name: this.i18nService.t("thirtyMinutes"), value: 30 },
|
||||
{ name: this.i18nService.t("oneHour"), value: 60 },
|
||||
{ name: this.i18nService.t("fourHours"), value: 240 },
|
||||
];
|
||||
|
||||
if (showOnLocked) {
|
||||
this.vaultTimeoutOptions.push({
|
||||
name: this.i18nService.t("onLocked"),
|
||||
value: VaultTimeoutStringType.OnLocked,
|
||||
});
|
||||
}
|
||||
|
||||
this.vaultTimeoutOptions.push({
|
||||
name: this.i18nService.t("onRestart"),
|
||||
value: VaultTimeoutStringType.OnRestart,
|
||||
});
|
||||
this.vaultTimeoutOptions.push({
|
||||
name: this.i18nService.t("never"),
|
||||
value: VaultTimeoutStringType.Never,
|
||||
});
|
||||
|
||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
|
||||
let timeout = await firstValueFrom(
|
||||
this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(activeAccount.id),
|
||||
);
|
||||
if (timeout === VaultTimeoutStringType.OnLocked && !showOnLocked) {
|
||||
timeout = VaultTimeoutStringType.OnRestart;
|
||||
}
|
||||
|
||||
const initialValues = {
|
||||
vaultTimeout: timeout,
|
||||
vaultTimeoutAction: await firstValueFrom(
|
||||
this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(activeAccount.id),
|
||||
),
|
||||
pin: await this.pinService.isPinSet(activeAccount.id),
|
||||
biometric: await this.vaultTimeoutSettingsService.isBiometricLockSet(),
|
||||
enableAutoBiometricsPrompt: await firstValueFrom(
|
||||
this.biometricStateService.promptAutomatically$,
|
||||
),
|
||||
};
|
||||
this.form.patchValue(initialValues, { emitEvent: false });
|
||||
|
||||
this.supportsBiometric = await this.biometricsService.supportsBiometric();
|
||||
this.showChangeMasterPass = await this.userVerificationService.hasMasterPassword();
|
||||
|
||||
this.form.controls.vaultTimeout.valueChanges
|
||||
.pipe(
|
||||
startWith(initialValues.vaultTimeout), // emit to init pairwise
|
||||
pairwise(),
|
||||
concatMap(async ([previousValue, newValue]) => {
|
||||
await this.saveVaultTimeout(previousValue, newValue);
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.form.controls.vaultTimeoutAction.valueChanges
|
||||
.pipe(
|
||||
startWith(initialValues.vaultTimeoutAction), // emit to init pairwise
|
||||
pairwise(),
|
||||
concatMap(async ([previousValue, newValue]) => {
|
||||
await this.saveVaultTimeoutAction(previousValue, newValue);
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.form.controls.pin.valueChanges
|
||||
.pipe(
|
||||
concatMap(async (value) => {
|
||||
await this.updatePin(value);
|
||||
this.refreshTimeoutSettings$.next();
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.form.controls.biometric.valueChanges
|
||||
.pipe(
|
||||
distinctUntilChanged(),
|
||||
concatMap(async (enabled) => {
|
||||
await this.updateBiometric(enabled);
|
||||
if (enabled) {
|
||||
this.form.controls.enableAutoBiometricsPrompt.enable();
|
||||
} else {
|
||||
this.form.controls.enableAutoBiometricsPrompt.disable();
|
||||
}
|
||||
this.refreshTimeoutSettings$.next();
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.refreshTimeoutSettings$
|
||||
.pipe(
|
||||
switchMap(() =>
|
||||
combineLatest([
|
||||
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(),
|
||||
this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(activeAccount.id),
|
||||
]),
|
||||
),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe(([availableActions, action]) => {
|
||||
this.availableVaultTimeoutActions = availableActions;
|
||||
this.form.controls.vaultTimeoutAction.setValue(action, { emitEvent: false });
|
||||
// NOTE: The UI doesn't properly update without detect changes.
|
||||
// I've even tried using an async pipe, but it still doesn't work. I'm not sure why.
|
||||
// Using an async pipe means that we can't call `detectChanges` AFTER the data has change
|
||||
// meaning that we are forced to use regular class variables instead of observables.
|
||||
this.changeDetectorRef.detectChanges();
|
||||
});
|
||||
|
||||
this.refreshTimeoutSettings$
|
||||
.pipe(
|
||||
switchMap(() =>
|
||||
combineLatest([
|
||||
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(),
|
||||
maximumVaultTimeoutPolicy,
|
||||
]),
|
||||
),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe(([availableActions, policy]) => {
|
||||
if (policy?.data?.action || availableActions.length <= 1) {
|
||||
this.form.controls.vaultTimeoutAction.disable({ emitEvent: false });
|
||||
} else {
|
||||
this.form.controls.vaultTimeoutAction.enable({ emitEvent: false });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async saveVaultTimeout(previousValue: VaultTimeout, newValue: VaultTimeout) {
|
||||
if (newValue === VaultTimeoutStringType.Never) {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "warning" },
|
||||
content: { key: "neverLockWarning" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
this.form.controls.vaultTimeout.setValue(previousValue, { emitEvent: false });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// The minTimeoutError does not apply to browser because it supports Immediately
|
||||
// So only check for the policyError
|
||||
if (this.form.controls.vaultTimeout.hasError("policyError")) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
null,
|
||||
this.i18nService.t("vaultTimeoutTooLarge"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
|
||||
const vaultTimeoutAction = await firstValueFrom(
|
||||
this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(activeAccount.id),
|
||||
);
|
||||
|
||||
await this.vaultTimeoutSettingsService.setVaultTimeoutOptions(
|
||||
activeAccount.id,
|
||||
newValue,
|
||||
vaultTimeoutAction,
|
||||
);
|
||||
if (newValue === VaultTimeoutStringType.Never) {
|
||||
this.messagingService.send("bgReseedStorage");
|
||||
}
|
||||
}
|
||||
|
||||
async saveVaultTimeoutAction(previousValue: VaultTimeoutAction, newValue: VaultTimeoutAction) {
|
||||
if (newValue === VaultTimeoutAction.LogOut) {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "vaultTimeoutLogOutConfirmationTitle" },
|
||||
content: { key: "vaultTimeoutLogOutConfirmation" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
this.form.controls.vaultTimeoutAction.setValue(previousValue, {
|
||||
emitEvent: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.form.controls.vaultTimeout.hasError("policyError")) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
null,
|
||||
this.i18nService.t("vaultTimeoutTooLarge"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
|
||||
await this.vaultTimeoutSettingsService.setVaultTimeoutOptions(
|
||||
activeAccount.id,
|
||||
this.form.value.vaultTimeout,
|
||||
newValue,
|
||||
);
|
||||
this.refreshTimeoutSettings$.next();
|
||||
}
|
||||
|
||||
async updatePin(value: boolean) {
|
||||
if (value) {
|
||||
const dialogRef = SetPinComponent.open(this.dialogService);
|
||||
|
||||
if (dialogRef == null) {
|
||||
this.form.controls.pin.setValue(false, { emitEvent: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const userHasPinSet = await firstValueFrom(dialogRef.closed);
|
||||
this.form.controls.pin.setValue(userHasPinSet, { emitEvent: false });
|
||||
} else {
|
||||
await this.vaultTimeoutSettingsService.clear();
|
||||
}
|
||||
}
|
||||
|
||||
async updateBiometric(enabled: boolean) {
|
||||
if (enabled && this.supportsBiometric) {
|
||||
let granted;
|
||||
try {
|
||||
granted = await BrowserApi.requestPermission({ permissions: ["nativeMessaging"] });
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line
|
||||
console.error(e);
|
||||
|
||||
if (this.platformUtilsService.isFirefox() && BrowserPopupUtils.inSidebar(window)) {
|
||||
await this.dialogService.openSimpleDialog({
|
||||
title: { key: "nativeMessaginPermissionSidebarTitle" },
|
||||
content: { key: "nativeMessaginPermissionSidebarDesc" },
|
||||
acceptButtonText: { key: "ok" },
|
||||
cancelButtonText: null,
|
||||
type: "info",
|
||||
});
|
||||
|
||||
this.form.controls.biometric.setValue(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!granted) {
|
||||
await this.dialogService.openSimpleDialog({
|
||||
title: { key: "nativeMessaginPermissionErrorTitle" },
|
||||
content: { key: "nativeMessaginPermissionErrorDesc" },
|
||||
acceptButtonText: { key: "ok" },
|
||||
cancelButtonText: null,
|
||||
type: "danger",
|
||||
});
|
||||
|
||||
this.form.controls.biometric.setValue(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const awaitDesktopDialogRef = AwaitDesktopDialogComponent.open(this.dialogService);
|
||||
const awaitDesktopDialogClosed = firstValueFrom(awaitDesktopDialogRef.closed);
|
||||
|
||||
await this.keyService.refreshAdditionalKeys();
|
||||
|
||||
await Promise.race([
|
||||
awaitDesktopDialogClosed.then(async (result) => {
|
||||
if (result !== true) {
|
||||
this.form.controls.biometric.setValue(false);
|
||||
}
|
||||
}),
|
||||
this.biometricsService
|
||||
.authenticateBiometric()
|
||||
.then((result) => {
|
||||
this.form.controls.biometric.setValue(result);
|
||||
if (!result) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorEnableBiometricTitle"),
|
||||
this.i18nService.t("errorEnableBiometricDesc"),
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
// Handle connection errors
|
||||
this.form.controls.biometric.setValue(false);
|
||||
|
||||
const error = BiometricErrors[e.message as BiometricErrorTypes];
|
||||
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.dialogService.openSimpleDialog({
|
||||
title: { key: error.title },
|
||||
content: { key: error.description },
|
||||
acceptButtonText: { key: "ok" },
|
||||
cancelButtonText: null,
|
||||
type: "danger",
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
awaitDesktopDialogRef.close(true);
|
||||
}),
|
||||
]);
|
||||
} else {
|
||||
await this.biometricStateService.setBiometricUnlockEnabled(false);
|
||||
await this.biometricStateService.setFingerprintValidated(false);
|
||||
}
|
||||
}
|
||||
|
||||
async updateAutoBiometricsPrompt() {
|
||||
await this.biometricStateService.setPromptAutomatically(
|
||||
this.form.value.enableAutoBiometricsPrompt,
|
||||
);
|
||||
}
|
||||
|
||||
async changePassword() {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "continueToWebApp" },
|
||||
content: { key: "changeMasterPasswordOnWebConfirmation" },
|
||||
type: "info",
|
||||
acceptButtonText: { key: "continue" },
|
||||
});
|
||||
if (confirmed) {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
await BrowserApi.createNewTab(env.getWebVaultUrl());
|
||||
}
|
||||
}
|
||||
|
||||
async twoStep() {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "twoStepLogin" },
|
||||
content: { key: "twoStepLoginConfirmation" },
|
||||
type: "info",
|
||||
});
|
||||
if (confirmed) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
BrowserApi.createNewTab("https://bitwarden.com/help/setup-two-step-login/");
|
||||
}
|
||||
}
|
||||
|
||||
async fingerprint() {
|
||||
const fingerprint = await this.keyService.getFingerprint(await this.stateService.getUserId());
|
||||
|
||||
const dialogRef = FingerprintDialogComponent.open(this.dialogService, {
|
||||
fingerprint,
|
||||
});
|
||||
|
||||
return firstValueFrom(dialogRef.closed);
|
||||
}
|
||||
|
||||
async lock() {
|
||||
await this.vaultTimeoutService.lock();
|
||||
}
|
||||
|
||||
async logOut() {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "logOut" },
|
||||
content: { key: "logOutConfirmation" },
|
||||
type: "info",
|
||||
});
|
||||
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
if (confirmed) {
|
||||
this.messagingService.send("logout", { userId: userId });
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
}
|
@ -60,7 +60,6 @@ import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||
import { enableAccountSwitching } from "../../../platform/flags";
|
||||
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
|
||||
import { PopupFooterComponent } from "../../../platform/popup/layout/popup-footer.component";
|
||||
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||
import { SetPinComponent } from "../components/set-pin.component";
|
||||
@ -82,7 +81,6 @@ import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component";
|
||||
JslibModule,
|
||||
LinkModule,
|
||||
PopOutComponent,
|
||||
PopupFooterComponent,
|
||||
PopupHeaderComponent,
|
||||
PopupPageComponent,
|
||||
RouterModule,
|
||||
|
@ -1,9 +1,4 @@
|
||||
<ng-container *ngIf="show && !useRefreshVariant">
|
||||
<button type="button" (click)="expand()" appA11yTitle="{{ 'popOutNewWindow' | i18n }}">
|
||||
<i class="bwi bwi-external-link bwi-rotate-270 bwi-lg bwi-fw" aria-hidden="true"></i>
|
||||
</button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="show && useRefreshVariant">
|
||||
<ng-container *ngIf="show">
|
||||
<button
|
||||
bitIconButton="bwi-popout"
|
||||
size="small"
|
||||
|
@ -2,8 +2,6 @@ import { CommonModule } from "@angular/common";
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { IconButtonModule } from "@bitwarden/components";
|
||||
|
||||
@ -17,16 +15,10 @@ import BrowserPopupUtils from "../browser-popup-utils";
|
||||
})
|
||||
export class PopOutComponent implements OnInit {
|
||||
@Input() show = true;
|
||||
useRefreshVariant = false;
|
||||
|
||||
constructor(
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
constructor(private platformUtilsService: PlatformUtilsService) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.useRefreshVariant = await this.configService.getFeatureFlag(FeatureFlag.ExtensionRefresh);
|
||||
|
||||
if (this.show) {
|
||||
if (
|
||||
(BrowserPopupUtils.inSidebar(window) && this.platformUtilsService.isFirefox()) ||
|
||||
|
@ -65,7 +65,6 @@ import { LoginViaAuthRequestComponentV1 } from "../auth/popup/login-via-auth-req
|
||||
import { RegisterComponent } from "../auth/popup/register.component";
|
||||
import { RemovePasswordComponent } from "../auth/popup/remove-password.component";
|
||||
import { SetPasswordComponent } from "../auth/popup/set-password.component";
|
||||
import { AccountSecurityComponent as AccountSecurityV1Component } from "../auth/popup/settings/account-security-v1.component";
|
||||
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
|
||||
import { SsoComponentV1 } from "../auth/popup/sso-v1.component";
|
||||
import { TwoFactorAuthComponent } from "../auth/popup/two-factor-auth.component";
|
||||
@ -95,28 +94,16 @@ import { ExportBrowserV2Component } from "../tools/popup/settings/export/export-
|
||||
import { ImportBrowserV2Component } from "../tools/popup/settings/import/import-browser-v2.component";
|
||||
import { SettingsV2Component } from "../tools/popup/settings/settings-v2.component";
|
||||
import { clearVaultStateGuard } from "../vault/guards/clear-vault-state.guard";
|
||||
import { AddEditComponent } from "../vault/popup/components/vault/add-edit.component";
|
||||
import { AttachmentsComponent } from "../vault/popup/components/vault/attachments.component";
|
||||
import { CollectionsComponent } from "../vault/popup/components/vault/collections.component";
|
||||
import { PasswordHistoryComponent } from "../vault/popup/components/vault/password-history.component";
|
||||
import { ShareComponent } from "../vault/popup/components/vault/share.component";
|
||||
import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component";
|
||||
import { VaultV2Component } from "../vault/popup/components/vault/vault-v2.component";
|
||||
import { ViewComponent } from "../vault/popup/components/vault/view.component";
|
||||
import { AddEditV2Component } from "../vault/popup/components/vault-v2/add-edit/add-edit-v2.component";
|
||||
import { AssignCollections } from "../vault/popup/components/vault-v2/assign-collections/assign-collections.component";
|
||||
import { AttachmentsV2Component } from "../vault/popup/components/vault-v2/attachments/attachments-v2.component";
|
||||
import { PasswordHistoryV2Component } from "../vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component";
|
||||
import { VaultV2Component } from "../vault/popup/components/vault-v2/vault-v2.component";
|
||||
import { ViewV2Component } from "../vault/popup/components/vault-v2/view-v2/view-v2.component";
|
||||
import { AppearanceV2Component } from "../vault/popup/settings/appearance-v2.component";
|
||||
import { AppearanceComponent } from "../vault/popup/settings/appearance.component";
|
||||
import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component";
|
||||
import { FoldersV2Component } from "../vault/popup/settings/folders-v2.component";
|
||||
import { FoldersComponent } from "../vault/popup/settings/folders.component";
|
||||
import { SyncComponent } from "../vault/popup/settings/sync.component";
|
||||
import { TrashComponent } from "../vault/popup/settings/trash.component";
|
||||
import { VaultSettingsV2Component } from "../vault/popup/settings/vault-settings-v2.component";
|
||||
import { VaultSettingsComponent } from "../vault/popup/settings/vault-settings.component";
|
||||
|
||||
import { RouteElevation } from "./app-routing.animations";
|
||||
import { debounceNavigationGuard } from "./services/debounce-navigation.service";
|
||||
@ -273,56 +260,43 @@ const routes: Routes = [
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "ciphers",
|
||||
component: VaultItemsComponent,
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
},
|
||||
...extensionRefreshSwap(ViewComponent, ViewV2Component, {
|
||||
path: "view-cipher",
|
||||
component: ViewV2Component,
|
||||
canActivate: [authGuard],
|
||||
data: {
|
||||
// Above "trash"
|
||||
elevation: 3,
|
||||
} satisfies RouteDataProperties,
|
||||
}),
|
||||
...extensionRefreshSwap(PasswordHistoryComponent, PasswordHistoryV2Component, {
|
||||
},
|
||||
{
|
||||
path: "cipher-password-history",
|
||||
component: PasswordHistoryV2Component,
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
}),
|
||||
...extensionRefreshSwap(AddEditComponent, AddEditV2Component, {
|
||||
},
|
||||
{
|
||||
path: "add-cipher",
|
||||
component: AddEditV2Component,
|
||||
canActivate: [authGuard, debounceNavigationGuard()],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
runGuardsAndResolvers: "always",
|
||||
}),
|
||||
...extensionRefreshSwap(AddEditComponent, AddEditV2Component, {
|
||||
},
|
||||
{
|
||||
path: "edit-cipher",
|
||||
component: AddEditV2Component,
|
||||
canActivate: [authGuard, debounceNavigationGuard()],
|
||||
data: {
|
||||
// Above "trash"
|
||||
elevation: 3,
|
||||
} satisfies RouteDataProperties,
|
||||
runGuardsAndResolvers: "always",
|
||||
}),
|
||||
{
|
||||
path: "share-cipher",
|
||||
component: ShareComponent,
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "collections",
|
||||
component: CollectionsComponent,
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
},
|
||||
...extensionRefreshSwap(AttachmentsComponent, AttachmentsV2Component, {
|
||||
path: "attachments",
|
||||
component: AttachmentsV2Component,
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: "generator",
|
||||
component: CredentialGeneratorComponent,
|
||||
@ -352,43 +326,28 @@ const routes: Routes = [
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
}),
|
||||
...extensionRefreshSwap(AccountSecurityV1Component, AccountSecurityComponent, {
|
||||
{
|
||||
path: "account-security",
|
||||
component: AccountSecurityComponent,
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
}),
|
||||
},
|
||||
...extensionRefreshSwap(NotificationsSettingsV1Component, NotificationsSettingsComponent, {
|
||||
path: "notifications",
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
}),
|
||||
...extensionRefreshSwap(VaultSettingsComponent, VaultSettingsV2Component, {
|
||||
{
|
||||
path: "vault-settings",
|
||||
component: VaultSettingsV2Component,
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
}),
|
||||
...extensionRefreshSwap(FoldersComponent, FoldersV2Component, {
|
||||
},
|
||||
{
|
||||
path: "folders",
|
||||
component: FoldersV2Component,
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 2 } satisfies RouteDataProperties,
|
||||
}),
|
||||
{
|
||||
path: "add-folder",
|
||||
component: FolderAddEditComponent,
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "edit-folder",
|
||||
component: FolderAddEditComponent,
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "sync",
|
||||
component: SyncComponent,
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "blocked-domains",
|
||||
@ -407,16 +366,18 @@ const routes: Routes = [
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
},
|
||||
...extensionRefreshSwap(AppearanceComponent, AppearanceV2Component, {
|
||||
{
|
||||
path: "appearance",
|
||||
component: AppearanceV2Component,
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
}),
|
||||
...extensionRefreshSwap(AddEditComponent, AddEditV2Component, {
|
||||
},
|
||||
{
|
||||
path: "clone-cipher",
|
||||
component: AddEditV2Component,
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: "add-send",
|
||||
component: SendAddEditV2Component,
|
||||
@ -692,7 +653,7 @@ const routes: Routes = [
|
||||
{
|
||||
path: "assign-collections",
|
||||
component: AssignCollections,
|
||||
canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh, true, "/")],
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
|
@ -29,7 +29,6 @@ import { LoginViaAuthRequestComponentV1 } from "../auth/popup/login-via-auth-req
|
||||
import { RegisterComponent } from "../auth/popup/register.component";
|
||||
import { RemovePasswordComponent } from "../auth/popup/remove-password.component";
|
||||
import { SetPasswordComponent } from "../auth/popup/set-password.component";
|
||||
import { AccountSecurityComponent as AccountSecurityComponentV1 } from "../auth/popup/settings/account-security-v1.component";
|
||||
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
|
||||
import { VaultTimeoutInputComponent } from "../auth/popup/settings/vault-timeout-input.component";
|
||||
import { SsoComponentV1 } from "../auth/popup/sso-v1.component";
|
||||
@ -55,25 +54,6 @@ import { PopupHeaderComponent } from "../platform/popup/layout/popup-header.comp
|
||||
import { PopupPageComponent } from "../platform/popup/layout/popup-page.component";
|
||||
import { PopupTabNavigationComponent } from "../platform/popup/layout/popup-tab-navigation.component";
|
||||
import { FilePopoutCalloutComponent } from "../tools/popup/components/file-popout-callout.component";
|
||||
import { ActionButtonsComponent } from "../vault/popup/components/action-buttons.component";
|
||||
import { CipherRowComponent } from "../vault/popup/components/cipher-row.component";
|
||||
import { AddEditCustomFieldsComponent } from "../vault/popup/components/vault/add-edit-custom-fields.component";
|
||||
import { AddEditComponent } from "../vault/popup/components/vault/add-edit.component";
|
||||
import { AttachmentsComponent } from "../vault/popup/components/vault/attachments.component";
|
||||
import { CollectionsComponent } from "../vault/popup/components/vault/collections.component";
|
||||
import { CurrentTabComponent } from "../vault/popup/components/vault/current-tab.component";
|
||||
import { PasswordHistoryComponent } from "../vault/popup/components/vault/password-history.component";
|
||||
import { ShareComponent } from "../vault/popup/components/vault/share.component";
|
||||
import { VaultFilterComponent } from "../vault/popup/components/vault/vault-filter.component";
|
||||
import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component";
|
||||
import { VaultSelectComponent } from "../vault/popup/components/vault/vault-select.component";
|
||||
import { ViewCustomFieldsComponent } from "../vault/popup/components/vault/view-custom-fields.component";
|
||||
import { ViewComponent } from "../vault/popup/components/vault/view.component";
|
||||
import { AppearanceComponent } from "../vault/popup/settings/appearance.component";
|
||||
import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component";
|
||||
import { FoldersComponent } from "../vault/popup/settings/folders.component";
|
||||
import { SyncComponent } from "../vault/popup/settings/sync.component";
|
||||
import { VaultSettingsComponent } from "../vault/popup/settings/vault-settings.component";
|
||||
|
||||
import { AppRoutingModule } from "./app-routing.module";
|
||||
import { AppComponent } from "./app.component";
|
||||
@ -128,49 +108,29 @@ import "../platform/popup/locales";
|
||||
ExtensionAnonLayoutWrapperComponent,
|
||||
],
|
||||
declarations: [
|
||||
ActionButtonsComponent,
|
||||
AddEditComponent,
|
||||
AddEditCustomFieldsComponent,
|
||||
AppComponent,
|
||||
AttachmentsComponent,
|
||||
CipherRowComponent,
|
||||
VaultItemsComponent,
|
||||
CollectionsComponent,
|
||||
ColorPasswordPipe,
|
||||
ColorPasswordCountPipe,
|
||||
CurrentTabComponent,
|
||||
EnvironmentComponent,
|
||||
ExcludedDomainsV1Component,
|
||||
Fido2CipherRowV1Component,
|
||||
Fido2UseBrowserLinkV1Component,
|
||||
FolderAddEditComponent,
|
||||
FoldersComponent,
|
||||
VaultFilterComponent,
|
||||
HintComponent,
|
||||
HomeComponent,
|
||||
LoginViaAuthRequestComponentV1,
|
||||
LoginComponentV1,
|
||||
LoginDecryptionOptionsComponentV1,
|
||||
NotificationsSettingsV1Component,
|
||||
AppearanceComponent,
|
||||
PasswordHistoryComponent,
|
||||
RegisterComponent,
|
||||
SetPasswordComponent,
|
||||
VaultSettingsComponent,
|
||||
ShareComponent,
|
||||
SsoComponentV1,
|
||||
SyncComponent,
|
||||
TabsV2Component,
|
||||
TwoFactorComponent,
|
||||
TwoFactorOptionsComponent,
|
||||
UpdateTempPasswordComponent,
|
||||
UserVerificationComponent,
|
||||
AccountSecurityComponentV1,
|
||||
VaultTimeoutInputComponent,
|
||||
ViewComponent,
|
||||
ViewCustomFieldsComponent,
|
||||
RemovePasswordComponent,
|
||||
VaultSelectComponent,
|
||||
Fido2V1Component,
|
||||
AutofillV1Component,
|
||||
EnvironmentSelectorComponent,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { inject } from "@angular/core";
|
||||
import { CanDeactivateFn } from "@angular/router";
|
||||
|
||||
import { VaultV2Component } from "../popup/components/vault/vault-v2.component";
|
||||
import { VaultV2Component } from "../popup/components/vault-v2/vault-v2.component";
|
||||
import { VaultPopupItemsService } from "../popup/services/vault-popup-items.service";
|
||||
import { VaultPopupListFiltersService } from "../popup/services/vault-popup-list-filters.service";
|
||||
|
||||
|
@ -1,102 +0,0 @@
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
(click)="view()"
|
||||
appStopClick
|
||||
appStopProp
|
||||
appA11yTitle="{{ 'view' | i18n }}"
|
||||
*ngIf="showView"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-list-alt" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ng-container *ngIf="cipher.type === cipherType.Login">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appStopProp
|
||||
appA11yTitle="{{ 'launch' | i18n }}"
|
||||
(click)="launchCipher()"
|
||||
*ngIf="!showView"
|
||||
[ngClass]="{ disabled: !cipher.login.canLaunch }"
|
||||
[attr.disabled]="!cipher.login.canLaunch ? '' : null"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-share-square" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appStopProp
|
||||
appA11yTitle="{{ 'copyUsername' | i18n }}"
|
||||
(click)="copy(cipher, cipher.login.username, 'username', 'Username')"
|
||||
[ngClass]="{ disabled: !cipher.login.username }"
|
||||
[attr.disabled]="!cipher.login.username ? '' : null"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-user" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appStopProp
|
||||
appA11yTitle="{{ 'copyPassword' | i18n }}"
|
||||
(click)="copy(cipher, cipher.login.password, 'password', 'Password')"
|
||||
[ngClass]="{ disabled: !cipher.login.password || !cipher.viewPassword }"
|
||||
[attr.disabled]="!cipher.login.password ? '' : null"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-key" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appStopProp
|
||||
appA11yTitle="{{ 'copyVerificationCode' | i18n }}"
|
||||
(click)="copy(cipher, cipher.login.totp, 'verificationCodeTotp', 'TOTP')"
|
||||
[ngClass]="{ disabled: !displayTotpCopyButton(cipher) }"
|
||||
[attr.disabled]="!displayTotpCopyButton(cipher) ? '' : null"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clock" aria-hidden="true"></i>
|
||||
</button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="cipher.type === cipherType.Card">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appStopProp
|
||||
appA11yTitle="{{ 'copyNumber' | i18n }}"
|
||||
(click)="copy(cipher, cipher.card.number, 'number', 'Card Number')"
|
||||
[ngClass]="{ disabled: !cipher.card.number }"
|
||||
[attr.disabled]="!cipher.card.number ? '' : null"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-hashtag" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appStopProp
|
||||
appA11yTitle="{{ 'copySecurityCode' | i18n }}"
|
||||
(click)="copy(cipher, cipher.card.code, 'securityCode', 'Security Code')"
|
||||
[ngClass]="{ disabled: !cipher.card.code }"
|
||||
[attr.disabled]="!cipher.card.code ? '' : null"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-key" aria-hidden="true"></i>
|
||||
</button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="cipher.type === cipherType.SecureNote">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appStopProp
|
||||
appA11yTitle="{{ 'copyNote' | i18n }}"
|
||||
(click)="copy(cipher, cipher.notes, 'note', 'Note')"
|
||||
[ngClass]="{ disabled: !cipher.notes }"
|
||||
[attr.disabled]="!cipher.notes ? '' : null"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</ng-container>
|
@ -1,108 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
@Component({
|
||||
selector: "app-action-buttons",
|
||||
templateUrl: "action-buttons.component.html",
|
||||
})
|
||||
export class ActionButtonsComponent implements OnInit, OnDestroy {
|
||||
@Output() onView = new EventEmitter<CipherView>();
|
||||
@Output() launchEvent = new EventEmitter<CipherView>();
|
||||
@Input() cipher: CipherView;
|
||||
@Input() showView = false;
|
||||
|
||||
cipherType = CipherType;
|
||||
userHasPremiumAccess = false;
|
||||
|
||||
private componentIsDestroyed$ = new Subject<boolean>();
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private eventCollectionService: EventCollectionService,
|
||||
private totpService: TotpServiceAbstraction,
|
||||
private passwordRepromptService: PasswordRepromptService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$
|
||||
.pipe(takeUntil(this.componentIsDestroyed$))
|
||||
.subscribe((canAccessPremium: boolean) => {
|
||||
this.userHasPremiumAccess = canAccessPremium;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.componentIsDestroyed$.next(true);
|
||||
this.componentIsDestroyed$.complete();
|
||||
}
|
||||
|
||||
launchCipher() {
|
||||
this.launchEvent.emit(this.cipher);
|
||||
}
|
||||
|
||||
async copy(cipher: CipherView, value: string, typeI18nKey: string, aType: string) {
|
||||
if (
|
||||
this.cipher.reprompt !== CipherRepromptType.None &&
|
||||
this.passwordRepromptService.protectedFields().includes(aType) &&
|
||||
!(await this.passwordRepromptService.showPasswordPrompt())
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value == null || (aType === "TOTP" && !this.displayTotpCopyButton(cipher))) {
|
||||
return;
|
||||
} else if (aType === "TOTP") {
|
||||
value = await this.totpService.getCode(value);
|
||||
}
|
||||
|
||||
if (!cipher.viewPassword) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.platformUtilsService.copyToClipboard(value, { window: window });
|
||||
this.platformUtilsService.showToast(
|
||||
"info",
|
||||
null,
|
||||
this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)),
|
||||
);
|
||||
|
||||
if (typeI18nKey === "password") {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.eventCollectionService.collect(EventType.Cipher_ClientCopiedPassword, cipher.id);
|
||||
} else if (typeI18nKey === "verificationCodeTotp") {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.eventCollectionService.collect(EventType.Cipher_ClientCopiedHiddenField, cipher.id);
|
||||
} else if (typeI18nKey === "securityCode") {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.eventCollectionService.collect(EventType.Cipher_ClientCopiedCardCode, cipher.id);
|
||||
}
|
||||
}
|
||||
|
||||
displayTotpCopyButton(cipher: CipherView) {
|
||||
return (
|
||||
(cipher?.login?.hasTotp ?? false) && (cipher.organizationUseTotp || this.userHasPremiumAccess)
|
||||
);
|
||||
}
|
||||
|
||||
view() {
|
||||
this.onView.emit(this.cipher);
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
<div
|
||||
role="group"
|
||||
appA11yTitle="{{ cipher.name }}"
|
||||
class="virtual-scroll-item"
|
||||
[ngClass]="{ 'override-last': !last }"
|
||||
>
|
||||
<div class="box-content-row box-content-row-flex">
|
||||
<button
|
||||
type="button"
|
||||
(click)="selectCipher(cipher)"
|
||||
(dblclick)="launchCipher(cipher)"
|
||||
appStopClick
|
||||
title="{{ title }} - {{ cipher.name }}"
|
||||
class="row-main"
|
||||
>
|
||||
<app-vault-icon [cipher]="cipher"></app-vault-icon>
|
||||
<div class="row-main-content">
|
||||
<span class="text">
|
||||
<span class="truncate-box">
|
||||
<span class="truncate">{{ cipher.name }}</span>
|
||||
<ng-container *ngIf="cipher.organizationId">
|
||||
<i
|
||||
class="bwi bwi-collection text-muted"
|
||||
title="{{ 'shared' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "shared" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="cipher.hasAttachments">
|
||||
<i
|
||||
class="bwi bwi-paperclip text-muted"
|
||||
title="{{ 'attachments' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "attachments" | i18n }}</span>
|
||||
</ng-container>
|
||||
</span>
|
||||
</span>
|
||||
<span class="detail">{{ cipher.subTitle }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<app-action-buttons
|
||||
[cipher]="cipher"
|
||||
[showView]="showView"
|
||||
(onView)="viewCipher(cipher)"
|
||||
(launchEvent)="launchCipher(cipher)"
|
||||
class="action-buttons"
|
||||
>
|
||||
</app-action-buttons>
|
||||
</div>
|
||||
</div>
|
@ -1,31 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
@Component({
|
||||
selector: "app-cipher-row",
|
||||
templateUrl: "cipher-row.component.html",
|
||||
})
|
||||
export class CipherRowComponent {
|
||||
@Output() onSelected = new EventEmitter<CipherView>();
|
||||
@Output() launchEvent = new EventEmitter<CipherView>();
|
||||
@Output() onView = new EventEmitter<CipherView>();
|
||||
@Input() cipher: CipherView;
|
||||
@Input() last: boolean;
|
||||
@Input() showView = false;
|
||||
@Input() title: string;
|
||||
|
||||
selectCipher(c: CipherView) {
|
||||
this.onSelected.emit(c);
|
||||
}
|
||||
|
||||
launchCipher(c: CipherView) {
|
||||
this.launchEvent.emit(c);
|
||||
}
|
||||
|
||||
viewCipher(c: CipherView) {
|
||||
this.onView.emit(c);
|
||||
}
|
||||
}
|
@ -39,7 +39,9 @@
|
||||
"
|
||||
class="{{ itemHeightClass }}"
|
||||
>
|
||||
<app-vault-icon slot="start" [cipher]="cipher"></app-vault-icon>
|
||||
<div slot="start" class="tw-justify-start tw-w-7 tw-flex">
|
||||
<app-vault-icon [cipher]="cipher"></app-vault-icon>
|
||||
</div>
|
||||
<span data-testid="item-name">{{ cipher.name }}</span>
|
||||
<i
|
||||
*ngIf="cipher.organizationId"
|
||||
|
@ -1,4 +1,5 @@
|
||||
<bit-search
|
||||
autocomplete="off"
|
||||
[placeholder]="'search' | i18n"
|
||||
[(ngModel)]="searchText"
|
||||
(ngModelChange)="onSearchTextChanged()"
|
||||
|
@ -19,12 +19,14 @@ import { VaultPopupAutofillService } from "../../services/vault-popup-autofill.s
|
||||
import { VaultPopupItemsService } from "../../services/vault-popup-items.service";
|
||||
import { VaultPopupListFiltersService } from "../../services/vault-popup-list-filters.service";
|
||||
import { VaultUiOnboardingService } from "../../services/vault-ui-onboarding.service";
|
||||
import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from "../vault-v2";
|
||||
|
||||
import {
|
||||
NewItemDropdownV2Component,
|
||||
NewItemInitialValues,
|
||||
} from "../vault-v2/new-item-dropdown/new-item-dropdown-v2.component";
|
||||
import { VaultHeaderV2Component } from "../vault-v2/vault-header/vault-header-v2.component";
|
||||
} from "./new-item-dropdown/new-item-dropdown-v2.component";
|
||||
import { VaultHeaderV2Component } from "./vault-header/vault-header-v2.component";
|
||||
|
||||
import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from ".";
|
||||
|
||||
enum VaultState {
|
||||
Empty,
|
@ -1,140 +0,0 @@
|
||||
<div class="box">
|
||||
<h2 class="box-header">
|
||||
{{ "customFields" | i18n }}
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<!-- Current custom fields -->
|
||||
<div cdkDropList (cdkDropListDropped)="drop($event)" *ngIf="cipher.hasFields">
|
||||
<div
|
||||
role="group"
|
||||
class="box-content-row box-content-row-multi box-draggable-row"
|
||||
appBoxRow
|
||||
cdkDrag
|
||||
*ngFor="let f of cipher.fields; let i = index; trackBy: trackByFunction"
|
||||
[ngClass]="{ 'box-content-row-checkbox': f.type === fieldType.Boolean }"
|
||||
attr.aria-label="{{ f.name }}"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
appStopClick
|
||||
(click)="removeField(f)"
|
||||
appA11yTitle="{{ 'remove' | i18n }}"
|
||||
*ngIf="!(!cipher.edit && editMode)"
|
||||
>
|
||||
<i class="bwi bwi-minus-circle bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<label for="fieldName{{ i }}" class="sr-only">{{ "name" | i18n }}</label>
|
||||
<label for="fieldValue{{ i }}" class="sr-only">{{ "value" | i18n }}</label>
|
||||
<div class="row-main">
|
||||
<input
|
||||
id="fieldName{{ i }}"
|
||||
type="text"
|
||||
name="Field.Name{{ i }}"
|
||||
[(ngModel)]="f.name"
|
||||
class="row-label"
|
||||
placeholder="{{ 'name' | i18n }}"
|
||||
appInputVerbatim
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
<!-- Text -->
|
||||
<input
|
||||
id="fieldValue{{ i }}"
|
||||
type="text"
|
||||
name="Field.Value{{ i }}"
|
||||
[(ngModel)]="f.value"
|
||||
*ngIf="f.type === fieldType.Text"
|
||||
placeholder="{{ 'value' | i18n }}"
|
||||
appInputVerbatim
|
||||
attr.aria-describedby="fieldName{{ i }}"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
<!-- Hidden -->
|
||||
<input
|
||||
id="fieldValue{{ i }}"
|
||||
type="{{ f.showValue ? 'text' : 'password' }}"
|
||||
name="Field.Value{{ i }}"
|
||||
[(ngModel)]="f.value"
|
||||
class="monospaced"
|
||||
appInputVerbatim
|
||||
*ngIf="f.type === fieldType.Hidden"
|
||||
placeholder="{{ 'value' | i18n }}"
|
||||
[disabled]="!cipher.viewPassword && !f.newField"
|
||||
attr.aria-describedby="fieldName{{ i }}"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
<!-- Linked -->
|
||||
<select
|
||||
id="fieldValue{{ i }}"
|
||||
name="Field.Value{{ i }}"
|
||||
[(ngModel)]="f.linkedId"
|
||||
*ngIf="f.type === fieldType.Linked && cipher.linkedFieldOptions != null"
|
||||
attr.aria-describedby="fieldName{{ i }}"
|
||||
>
|
||||
<option *ngFor="let o of linkedFieldOptions" [ngValue]="o.value">{{ o.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Boolean -->
|
||||
<input
|
||||
id="fieldValue{{ i }}"
|
||||
name="Field.Value{{ i }}"
|
||||
type="checkbox"
|
||||
[(ngModel)]="f.value"
|
||||
*ngIf="f.type === fieldType.Boolean"
|
||||
appTrueFalseValue
|
||||
trueValue="true"
|
||||
falseValue="false"
|
||||
attr.aria-describedby="fieldName{{ i }}"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
<div
|
||||
class="action-buttons"
|
||||
*ngIf="f.type === fieldType.Hidden && (cipher.viewPassword || f.newField)"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
(click)="toggleFieldValue(f)"
|
||||
[attr.aria-pressed]="f.showValue"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !f.showValue, 'bwi-eye-slash': f.showValue }"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="drag-handle"
|
||||
appA11yTitle="{{ 'dragToSort' | i18n }}"
|
||||
*ngIf="!(!cipher.edit && editMode)"
|
||||
cdkDragHandle
|
||||
>
|
||||
<i class="bwi bwi-hamburger" aria-hidden="true"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Add new custom field -->
|
||||
<div
|
||||
class="box-content-row box-content-row-newmulti"
|
||||
*ngIf="!(!cipher.edit && editMode)"
|
||||
appBoxRow
|
||||
>
|
||||
<button type="button" appStopClick (click)="addField()">
|
||||
<i class="bwi bwi-plus-circle bwi-fw bwi-lg" aria-hidden="true"></i>
|
||||
{{ "newCustomField" | i18n }}
|
||||
</button>
|
||||
<label for="addFieldType" class="sr-only">{{ "type" | i18n }}</label>
|
||||
<select id="addFieldType" name="AddFieldType" [(ngModel)]="addFieldType" class="field-type">
|
||||
<option *ngFor="let o of addFieldTypeOptions" [ngValue]="o.value">{{ o.name }}</option>
|
||||
<option
|
||||
*ngIf="cipher.linkedFieldOptions != null"
|
||||
[ngValue]="addFieldLinkedTypeOption.value"
|
||||
>
|
||||
{{ addFieldLinkedTypeOption.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,15 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { AddEditCustomFieldsComponent as BaseAddEditCustomFieldsComponent } from "@bitwarden/angular/vault/components/add-edit-custom-fields.component";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-add-edit-custom-fields",
|
||||
templateUrl: "add-edit-custom-fields.component.html",
|
||||
})
|
||||
export class AddEditCustomFieldsComponent extends BaseAddEditCustomFieldsComponent {
|
||||
constructor(i18nService: I18nService, eventCollectionService: EventCollectionService) {
|
||||
super(i18nService, eventCollectionService);
|
||||
}
|
||||
}
|
@ -1,826 +0,0 @@
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||
<header>
|
||||
<div class="left">
|
||||
<button type="button" (click)="cancel()">{{ "cancel" | i18n }}</button>
|
||||
</div>
|
||||
<h1 class="center">
|
||||
<span class="title">{{ title }}</span>
|
||||
</h1>
|
||||
<div class="right">
|
||||
<button type="submit" [disabled]="form.loading">
|
||||
<span [hidden]="form.loading">{{ "save" | i18n }}</span>
|
||||
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<main tabindex="-1" *ngIf="cipher">
|
||||
<app-callout type="info" *ngIf="allowOwnershipOptions() && !allowPersonal">
|
||||
{{ "personalOwnershipPolicyInEffect" | i18n }}
|
||||
</app-callout>
|
||||
<div class="box">
|
||||
<h2 class="box-header">
|
||||
{{ "itemInformation" | i18n }}
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<div class="box-content-row" *ngIf="!editMode" appBoxRow>
|
||||
<label for="type">{{ "type" | i18n }}</label>
|
||||
<select id="type" name="Type" [(ngModel)]="cipher.type">
|
||||
<option *ngFor="let o of typeOptions" [ngValue]="o.value">{{ o.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="name">{{ "name" | i18n }}</label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
name="Name"
|
||||
[(ngModel)]="cipher.name"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<!-- Login -->
|
||||
<div *ngIf="cipher.type === cipherType.Login">
|
||||
<div class="box-content-row box-content-row-flex" appBoxRow>
|
||||
<div class="row-main">
|
||||
<label for="loginUsername">{{ "username" | i18n }}</label>
|
||||
<input
|
||||
id="loginUsername"
|
||||
type="text"
|
||||
name="Login.Username"
|
||||
[(ngModel)]="cipher.login.username"
|
||||
inputmode="email"
|
||||
appInputVerbatim
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'generateUsername' | i18n }}"
|
||||
(click)="generateUsername()"
|
||||
*ngIf="!(!cipher.edit && editMode)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-lg bwi-generate" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-flex" appBoxRow>
|
||||
<div class="row-main">
|
||||
<label for="loginPassword">{{ "password" | i18n }}</label>
|
||||
<input
|
||||
id="loginPassword"
|
||||
class="monospaced"
|
||||
type="{{ showPassword ? 'text' : 'password' }}"
|
||||
name="Login.Password"
|
||||
[(ngModel)]="cipher.login.password"
|
||||
appInputVerbatim
|
||||
[disabled]="!cipher.viewPassword"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
#checkPasswordBtn
|
||||
class="row-btn btn"
|
||||
appA11yTitle="{{ 'checkPassword' | i18n }}"
|
||||
(click)="checkPassword()"
|
||||
[appApiAction]="checkPasswordPromise"
|
||||
[disabled]="$any(checkPasswordBtn).loading"
|
||||
*ngIf="cipher.viewPassword"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw bwi-lg bwi-check-circle"
|
||||
[hidden]="$any(checkPasswordBtn).loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<i
|
||||
class="bwi bwi-fw bwi-lg bwi-spinner bwi-spin"
|
||||
[hidden]="!$any(checkPasswordBtn).loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
(click)="togglePassword()"
|
||||
*ngIf="cipher.viewPassword && cipher.login.password"
|
||||
[attr.aria-pressed]="showPassword"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'generatePassword' | i18n }}"
|
||||
(click)="generatePassword()"
|
||||
*ngIf="cipher.viewPassword && !(!cipher.edit && editMode)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-lg bwi-generate" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--Passkey-->
|
||||
<div
|
||||
class="box"
|
||||
*ngIf="cipher.login.hasFido2Credentials && !cloneMode"
|
||||
tabindex="0"
|
||||
attr.aria-label="{{ 'typePasskey' | i18n }} {{ fido2CredentialCreationDateValue }}"
|
||||
>
|
||||
<div class="box-content">
|
||||
<div class="box-content-row box-content-row-multi text-muted" appBoxRow>
|
||||
<button
|
||||
type="button"
|
||||
appStopClick
|
||||
(click)="removePasskey()"
|
||||
appA11yTitle="{{ 'removePasskey' | i18n }}"
|
||||
*ngIf="!(!cipher.edit && editMode)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-minus-circle bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="row-main">
|
||||
<span class="row-label">{{ "typePasskey" | i18n }}</span>
|
||||
{{ "dateCreated" | i18n }}
|
||||
{{ cipher.login.fido2Credentials[0].creationDate | date: "short" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box-content-row box-content-row-flex" appBoxRow>
|
||||
<div class="row-main">
|
||||
<label for="loginTotp">{{ "authenticatorKeyTotp" | i18n }}</label>
|
||||
<input
|
||||
id="loginTotp"
|
||||
type="{{ showTotpSeed ? 'text' : 'password' }}"
|
||||
name="Login.Totp"
|
||||
class="monospaced"
|
||||
[(ngModel)]="cipher.login.totp"
|
||||
appInputVerbatim
|
||||
[disabled]="!cipher.viewPassword"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
(click)="toggleTotpSeed()"
|
||||
*ngIf="cipher.viewPassword && cipher.login.totp"
|
||||
[attr.aria-pressed]="showTotpSeed"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !showTotpSeed, 'bwi-eye-slash': showTotpSeed }"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'copyTOTP' | i18n }}"
|
||||
(click)="copy(cipher.login.totp, 'totp', 'TOTP')"
|
||||
*ngIf="cipher.viewPassword"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'totpCapture' | i18n }}"
|
||||
(click)="captureTOTPFromTab()"
|
||||
*ngIf="!(!cipher.edit && editMode)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-lg bwi-camera" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card -->
|
||||
<div *ngIf="cipher.type === cipherType.Card">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="cardCardholderName">{{ "cardholderName" | i18n }}</label>
|
||||
<input
|
||||
id="cardCardholderName"
|
||||
type="text"
|
||||
name="Card.CardCardholderName"
|
||||
[(ngModel)]="cipher.card.cardholderName"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-flex" appBoxRow>
|
||||
<div class="row-main">
|
||||
<label for="cardNumber">{{ "number" | i18n }}</label>
|
||||
<input
|
||||
id="cardNumber"
|
||||
class="monospaced"
|
||||
type="{{ showCardNumber ? 'text' : 'password' }}"
|
||||
name="Card.Number"
|
||||
(input)="onCardNumberChange()"
|
||||
[(ngModel)]="cipher.card.number"
|
||||
appInputVerbatim
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
(click)="toggleCardNumber()"
|
||||
[attr.aria-pressed]="showCardNumber"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !showCardNumber, 'bwi-eye-slash': showCardNumber }"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="cardBrand">{{ "brand" | i18n }}</label>
|
||||
<span *ngIf="!(!cipher.edit && editMode); else readonlyCardBrand">
|
||||
<select id="cardBrand" name="Card.Brand" [(ngModel)]="cipher.card.brand">
|
||||
<option *ngFor="let o of cardBrandOptions" [ngValue]="o.value">{{ o.name }}</option>
|
||||
</select>
|
||||
</span>
|
||||
<ng-template #readonlyCardBrand>
|
||||
<input
|
||||
id="cardBrand"
|
||||
name="Card.Brand"
|
||||
type="text"
|
||||
[readonly]="true"
|
||||
[value]="cipher.card.brand"
|
||||
/>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="cardExpMonth">{{ "expirationMonth" | i18n }}</label>
|
||||
<span *ngIf="!(!cipher.edit && editMode); else readonlyCardExpMonth">
|
||||
<select id="cardExpMonth" name="Card.ExpMonth" [(ngModel)]="cipher.card.expMonth">
|
||||
<option *ngFor="let o of cardExpMonthOptions" [ngValue]="o.value">
|
||||
{{ o.name }}
|
||||
</option>
|
||||
</select>
|
||||
</span>
|
||||
<ng-template #readonlyCardExpMonth>
|
||||
<input
|
||||
id="cardExpMonth"
|
||||
name="Card.ExpMonth"
|
||||
type="text"
|
||||
[readonly]="true"
|
||||
[value]="getCardExpMonthDisplay()"
|
||||
/>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="cardExpYear">{{ "expirationYear" | i18n }}</label>
|
||||
<input
|
||||
id="cardExpYear"
|
||||
type="text"
|
||||
name="Card.ExpYear"
|
||||
[(ngModel)]="cipher.card.expYear"
|
||||
placeholder="{{ 'ex' | i18n }} {{ currentDate | date: 'yyyy' }}"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-flex" appBoxRow>
|
||||
<div class="row-main">
|
||||
<label for="cardCode">{{ "securityCode" | i18n }}</label>
|
||||
<input
|
||||
id="cardCode"
|
||||
class="monospaced"
|
||||
type="{{ showCardCode ? 'text' : 'password' }}"
|
||||
name="Card.Code"
|
||||
[(ngModel)]="cipher.card.code"
|
||||
appInputVerbatim
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
(click)="toggleCardCode()"
|
||||
[attr.aria-pressed]="showCardCode"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !showCardCode, 'bwi-eye-slash': showCardCode }"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Identity -->
|
||||
<div *ngIf="cipher.type === cipherType.Identity">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idTitle">{{ "title" | i18n }}</label>
|
||||
<span *ngIf="!(!cipher.edit && editMode); else readonlyIdTitle">
|
||||
<select id="idTitle" name="Identity.Title" [(ngModel)]="cipher.identity.title">
|
||||
<option *ngFor="let o of identityTitleOptions" [ngValue]="o.value">
|
||||
{{ o.name }}
|
||||
</option>
|
||||
</select>
|
||||
</span>
|
||||
<ng-template #readonlyIdTitle>
|
||||
<input
|
||||
id="idTitle"
|
||||
name="Identity.Title"
|
||||
type="text"
|
||||
[readonly]="true"
|
||||
[value]="cipher.identity.title"
|
||||
/>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idFirstName">{{ "firstName" | i18n }}</label>
|
||||
<input
|
||||
id="idFirstName"
|
||||
type="text"
|
||||
name="Identity.FirstName"
|
||||
[(ngModel)]="cipher.identity.firstName"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idMiddleName">{{ "middleName" | i18n }}</label>
|
||||
<input
|
||||
id="idMiddleName"
|
||||
type="text"
|
||||
name="Identity.MiddleName"
|
||||
[(ngModel)]="cipher.identity.middleName"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idLastName">{{ "lastName" | i18n }}</label>
|
||||
<input
|
||||
id="idLastName"
|
||||
type="text"
|
||||
name="Identity.LastName"
|
||||
[(ngModel)]="cipher.identity.lastName"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idUsername">{{ "username" | i18n }}</label>
|
||||
<input
|
||||
id="idUsername"
|
||||
type="text"
|
||||
name="Identity.Username"
|
||||
[(ngModel)]="cipher.identity.username"
|
||||
appInputVerbatim
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idCompany">{{ "company" | i18n }}</label>
|
||||
<input
|
||||
id="idCompany"
|
||||
type="text"
|
||||
name="Identity.Company"
|
||||
[(ngModel)]="cipher.identity.company"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idSsn">{{ "ssn" | i18n }}</label>
|
||||
<input
|
||||
id="idSsn"
|
||||
type="text"
|
||||
name="Identity.SSN"
|
||||
[(ngModel)]="cipher.identity.ssn"
|
||||
appInputVerbatim
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idPassportNumber">{{ "passportNumber" | i18n }}</label>
|
||||
<input
|
||||
id="idPassportNumber"
|
||||
type="text"
|
||||
name="Identity.PassportNumber"
|
||||
[(ngModel)]="cipher.identity.passportNumber"
|
||||
appInputVerbatim
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idLicenseNumber">{{ "licenseNumber" | i18n }}</label>
|
||||
<input
|
||||
id="idLicenseNumber"
|
||||
type="text"
|
||||
name="Identity.LicenseNumber"
|
||||
[(ngModel)]="cipher.identity.licenseNumber"
|
||||
appInputVerbatim
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idEmail">{{ "email" | i18n }}</label>
|
||||
<input
|
||||
id="idEmail"
|
||||
type="text"
|
||||
name="Identity.Email"
|
||||
[(ngModel)]="cipher.identity.email"
|
||||
appInputVerbatim
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idPhone">{{ "phone" | i18n }}</label>
|
||||
<input
|
||||
id="idPhone"
|
||||
type="text"
|
||||
name="Identity.Phone"
|
||||
[(ngModel)]="cipher.identity.phone"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idAddress1">{{ "address1" | i18n }}</label>
|
||||
<input
|
||||
id="idAddress1"
|
||||
type="text"
|
||||
name="Identity.Address1"
|
||||
[(ngModel)]="cipher.identity.address1"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idAddress2">{{ "address2" | i18n }}</label>
|
||||
<input
|
||||
id="idAddress2"
|
||||
type="text"
|
||||
name="Identity.Address2"
|
||||
[(ngModel)]="cipher.identity.address2"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idAddress3">{{ "address3" | i18n }}</label>
|
||||
<input
|
||||
id="idAddress3"
|
||||
type="text"
|
||||
name="Identity.Address3"
|
||||
[(ngModel)]="cipher.identity.address3"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idCity">{{ "cityTown" | i18n }}</label>
|
||||
<input
|
||||
id="idCity"
|
||||
type="text"
|
||||
name="Identity.City"
|
||||
[(ngModel)]="cipher.identity.city"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idState">{{ "stateProvince" | i18n }}</label>
|
||||
<input
|
||||
id="idState"
|
||||
type="text"
|
||||
name="Identity.State"
|
||||
[(ngModel)]="cipher.identity.state"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idPostalCode">{{ "zipPostalCode" | i18n }}</label>
|
||||
<input
|
||||
id="idPostalCode"
|
||||
type="text"
|
||||
name="Identity.PostalCode"
|
||||
[(ngModel)]="cipher.identity.postalCode"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idCountry">{{ "country" | i18n }}</label>
|
||||
<input
|
||||
id="idCountry"
|
||||
type="text"
|
||||
name="Identity.Country"
|
||||
[(ngModel)]="cipher.identity.country"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SshKey -->
|
||||
<div *ngIf="cipher.sshKey">
|
||||
<div class="box-content-row" *ngIf="cipher.sshKey.privateKey" style="overflow: hidden">
|
||||
<span class="row-label"> {{ "sshPrivateKey" | i18n }}</span>
|
||||
{{ cipher.sshKey.privateKey }}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.sshKey.publicKey" style="overflow: hidden">
|
||||
<span class="row-label"> {{ "sshPublicKey" | i18n }}</span>
|
||||
{{ cipher.sshKey.publicKey }}
|
||||
</div>
|
||||
<div
|
||||
class="box-content-row"
|
||||
*ngIf="cipher.sshKey.keyFingerprint"
|
||||
style="overflow: hidden"
|
||||
>
|
||||
<span class="row-label"> {{ "sshKeyFingerprint" | i18n }}</span>
|
||||
{{ cipher.sshKey.keyFingerprint }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" *ngIf="cipher.type === cipherType.Login">
|
||||
<div class="box-content">
|
||||
<ng-container *ngIf="cipher.login.hasUris">
|
||||
<div
|
||||
role="group"
|
||||
class="box-content-row box-content-row-multi"
|
||||
appBoxRow
|
||||
*ngFor="let u of cipher.login.uris; let i = index; trackBy: trackByFunction"
|
||||
attr.aria-label="{{ 'uriPosition' | i18n: i + 1 }}"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
*ngIf="!(!cipher.edit && editMode)"
|
||||
appStopClick
|
||||
(click)="removeUri(u)"
|
||||
appA11yTitle="{{ 'remove' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-minus-circle bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="row-main">
|
||||
<label for="loginUri{{ i }}">{{ "uriPosition" | i18n: i + 1 }}</label>
|
||||
<input
|
||||
id="loginUri{{ i }}"
|
||||
type="text"
|
||||
name="Login.Uris[{{ i }}].Uri"
|
||||
[(ngModel)]="u.uri"
|
||||
[hidden]="$any(u).showUriOptionsInput === true"
|
||||
placeholder="{{ 'ex' | i18n }} https://google.com"
|
||||
inputmode="url"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
appInputVerbatim
|
||||
/>
|
||||
<label for="loginUriMatch{{ i }}" class="sr-only">
|
||||
{{ "currentUri" | i18n }} {{ i + 1 }}
|
||||
</label>
|
||||
<select
|
||||
*ngIf="currentUris && currentUris.length"
|
||||
id="currentUris{{ i }}"
|
||||
name="Login.Uris[{{ i }}].CurrentUris"
|
||||
[(ngModel)]="u.uri"
|
||||
[hidden]="!$any(u).showCurrentUris"
|
||||
>
|
||||
<option [ngValue]="null">-- {{ "select" | i18n }} --</option>
|
||||
<option *ngFor="let u of currentUris" [ngValue]="u">{{ u }}</option>
|
||||
</select>
|
||||
<label for="loginUriMatch{{ i }}" class="sr-only">
|
||||
{{ "matchDetection" | i18n }} {{ i + 1 }}
|
||||
</label>
|
||||
<select
|
||||
id="loginUriMatch{{ i }}"
|
||||
name="Login.Uris[{{ i }}].Match"
|
||||
[(ngModel)]="u.match"
|
||||
[hidden]="
|
||||
$any(u).showOptions === false || ($any(u).showOptions == null && u.match == null)
|
||||
"
|
||||
(change)="loginUriMatchChanged(u)"
|
||||
>
|
||||
<option *ngFor="let o of uriMatchOptions" [ngValue]="o.value">{{ o.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
*ngIf="currentUris && currentUris.length"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'toggleCurrentUris' | i18n }}"
|
||||
(click)="toggleUriInput(u)"
|
||||
[attr.aria-pressed]="$any(u).showCurrentUris === true"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-fw bwi-lg bwi-list"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'toggleOptions' | i18n }}"
|
||||
(click)="toggleUriOptions(u)"
|
||||
[attr.aria-pressed]="$any(u).showOptions === true"
|
||||
[disabled]="!cipher.edit && editMode"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-lg bwi-cog" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<button
|
||||
type="button"
|
||||
appStopClick
|
||||
(click)="addUri()"
|
||||
class="box-content-row box-content-row-newmulti single-line"
|
||||
*ngIf="!(!cipher.edit && editMode)"
|
||||
>
|
||||
<i class="bwi bwi-plus-circle bwi-fw bwi-lg" aria-hidden="true"></i> {{ "newUri" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" *ngIf="showAutoFillOnPageLoadOptions">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="autofillOnPageLoad">{{ "itemAutoFillOnPageLoad" | i18n }} </label>
|
||||
<select
|
||||
id="autofillOnPageLoad"
|
||||
name="AutofillOnPageLoad"
|
||||
[disabled]="reprompt"
|
||||
[(ngModel)]="cipher.login.autofillOnPageLoad"
|
||||
>
|
||||
<option *ngFor="let o of autofillOnPageLoadOptions" [ngValue]="o.value">
|
||||
{{ o.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-footer !tw-mb-0 !tw-pb-0" *ngIf="reprompt">
|
||||
{{ "turnOffMasterPasswordPromptToEditField" | i18n }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="folder">{{ "folder" | i18n }}</label>
|
||||
<select id="folder" name="FolderId" [(ngModel)]="cipher.folderId">
|
||||
<option *ngFor="let f of folders$ | async" [ngValue]="f.id">{{ f.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||
<label for="favorite">{{ "favorite" | i18n }}</label>
|
||||
<input id="favorite" type="checkbox" name="Favorite" [(ngModel)]="cipher.favorite" />
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-checkbox" appBoxRow *ngIf="canUseReprompt">
|
||||
<label for="passwordPrompt">
|
||||
{{ "passwordPrompt" | i18n }}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
||||
href="https://bitwarden.com/help/managing-items/#protect-individual-items"
|
||||
>
|
||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||
</a>
|
||||
</label>
|
||||
<input
|
||||
id="passwordPrompt"
|
||||
type="checkbox"
|
||||
name="PasswordPrompt"
|
||||
[ngModel]="reprompt"
|
||||
(change)="repromptChanged()"
|
||||
[disabled]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row box-content-row-flex text-default single-line"
|
||||
appStopClick
|
||||
(click)="attachments()"
|
||||
*ngIf="editMode && showAttachments && !cloneMode"
|
||||
>
|
||||
<div class="row-main">{{ "attachments" | i18n }}</div>
|
||||
<i
|
||||
class="bwi bwi-external-link bwi-lg bwi-fw"
|
||||
aria-hidden="true"
|
||||
*ngIf="openAttachmentsInPopup"
|
||||
></i>
|
||||
<i
|
||||
class="bwi bwi-angle-right row-sub-icon"
|
||||
aria-hidden="true"
|
||||
*ngIf="!openAttachmentsInPopup"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row box-content-row-flex text-default"
|
||||
appStopClick
|
||||
(click)="editCollections()"
|
||||
*ngIf="editMode && cipher.organizationId && !cloneMode"
|
||||
>
|
||||
<div class="row-main">{{ "collections" | i18n }}</div>
|
||||
<i class="bwi bwi-angle-right row-sub-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<h2 class="box-header">
|
||||
<label for="notes">{{ "notes" | i18n }}</label>
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<textarea
|
||||
id="notes"
|
||||
name="Notes"
|
||||
rows="6"
|
||||
[(ngModel)]="cipher.notes"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<app-vault-add-edit-custom-fields
|
||||
*ngIf="!(!cipher.hasFields && !cipher.edit && editMode)"
|
||||
[cipher]="cipher"
|
||||
[thisCipherType]="cipher.type"
|
||||
[editMode]="editMode"
|
||||
>
|
||||
</app-vault-add-edit-custom-fields>
|
||||
<div class="box" *ngIf="allowOwnershipOptions()">
|
||||
<h2 class="box-header">
|
||||
{{ "ownership" | i18n }}
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="organizationId">{{ "whoOwnsThisItem" | i18n }}</label>
|
||||
<select
|
||||
id="organizationId"
|
||||
class="form-control"
|
||||
name="OrganizationId"
|
||||
[(ngModel)]="cipher.organizationId"
|
||||
(change)="organizationChanged()"
|
||||
>
|
||||
<option *ngFor="let o of ownershipOptions" [ngValue]="o.value">{{ o.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" *ngIf="(!editMode || cloneMode) && cipher.organizationId">
|
||||
<h2 class="box-header">
|
||||
{{ "collections" | i18n }}
|
||||
</h2>
|
||||
<div class="box-content" *ngIf="!collections || !collections.length">
|
||||
<div class="box-content-row padded no-hover">
|
||||
{{ "noCollectionsInList" | i18n }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content" *ngIf="collections && collections.length">
|
||||
<div
|
||||
class="box-content-row box-content-row-checkbox"
|
||||
*ngFor="let c of collections; let i = index"
|
||||
appBoxRow
|
||||
>
|
||||
<label for="collection_{{ i }}">{{ c.name }}</label>
|
||||
<input
|
||||
id="collection_{{ i }}"
|
||||
type="checkbox"
|
||||
[(ngModel)]="$any(c).checked"
|
||||
name="Collection[{{ i }}].Checked"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box list" *ngIf="editMode && !cloneMode && (canDeleteCipher$ | async)">
|
||||
<div class="box-content single-line">
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row"
|
||||
appStopClick
|
||||
(click)="delete()"
|
||||
[appApiAction]="deletePromise"
|
||||
#deleteBtn
|
||||
>
|
||||
<div class="row-main text-danger">
|
||||
<div class="icon text-danger" aria-hidden="true">
|
||||
<i class="bwi bwi-trash bwi-lg bwi-fw" [hidden]="$any(deleteBtn).loading"></i>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
|
||||
[hidden]="!$any(deleteBtn).loading"
|
||||
></i>
|
||||
</div>
|
||||
<span>{{ "deleteItem" | i18n }}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</form>
|
@ -1,417 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DatePipe, Location } from "@angular/common";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import qrcodeParser from "qrcode-parser";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/vault/components/add-edit.component";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.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";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
import { normalizeExpiryYearFormat } from "@bitwarden/common/autofill/utils";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { BrowserFido2UserInterfaceSession } from "../../../../autofill/fido2/services/browser-fido2-user-interface.service";
|
||||
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils";
|
||||
import { PopupCloseWarningService } from "../../../../popup/services/popup-close-warning.service";
|
||||
import { Fido2UserVerificationService } from "../../../services/fido2-user-verification.service";
|
||||
import { fido2PopoutSessionData$ } from "../../utils/fido2-popout-session-data";
|
||||
import { closeAddEditVaultItemPopout, VaultPopoutType } from "../../utils/vault-popout-window";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-add-edit",
|
||||
templateUrl: "add-edit.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class AddEditComponent extends BaseAddEditComponent implements OnInit {
|
||||
currentUris: string[];
|
||||
showAttachments = true;
|
||||
openAttachmentsInPopup: boolean;
|
||||
showAutoFillOnPageLoadOptions: boolean;
|
||||
|
||||
private fido2PopoutSessionData$ = fido2PopoutSessionData$();
|
||||
|
||||
constructor(
|
||||
cipherService: CipherService,
|
||||
folderService: FolderService,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
auditService: AuditService,
|
||||
accountService: AccountService,
|
||||
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
||||
collectionService: CollectionService,
|
||||
messagingService: MessagingService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private location: Location,
|
||||
eventCollectionService: EventCollectionService,
|
||||
policyService: PolicyService,
|
||||
private popupCloseWarningService: PopupCloseWarningService,
|
||||
organizationService: OrganizationService,
|
||||
passwordRepromptService: PasswordRepromptService,
|
||||
logService: LogService,
|
||||
dialogService: DialogService,
|
||||
datePipe: DatePipe,
|
||||
configService: ConfigService,
|
||||
private fido2UserVerificationService: Fido2UserVerificationService,
|
||||
cipherAuthorizationService: CipherAuthorizationService,
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
folderService,
|
||||
i18nService,
|
||||
platformUtilsService,
|
||||
auditService,
|
||||
accountService,
|
||||
collectionService,
|
||||
messagingService,
|
||||
eventCollectionService,
|
||||
policyService,
|
||||
logService,
|
||||
passwordRepromptService,
|
||||
organizationService,
|
||||
dialogService,
|
||||
window,
|
||||
datePipe,
|
||||
configService,
|
||||
cipherAuthorizationService,
|
||||
);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await super.ngOnInit();
|
||||
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.queryParams.pipe(first()).subscribe(async (params) => {
|
||||
if (params.cipherId) {
|
||||
this.cipherId = params.cipherId;
|
||||
}
|
||||
if (params.folderId) {
|
||||
this.folderId = params.folderId;
|
||||
}
|
||||
if (params.collectionId) {
|
||||
this.collectionId = params.collectionId;
|
||||
const collection = this.writeableCollections.find((c) => c.id === params.collectionId);
|
||||
if (collection != null) {
|
||||
this.collectionIds = [collection.id];
|
||||
this.organizationId = collection.organizationId;
|
||||
}
|
||||
}
|
||||
if (params.type) {
|
||||
const type = parseInt(params.type, null);
|
||||
this.type = type;
|
||||
}
|
||||
this.editMode = !params.cipherId;
|
||||
|
||||
if (params.cloneMode != null) {
|
||||
this.cloneMode = params.cloneMode === "true";
|
||||
}
|
||||
if (params.selectedVault) {
|
||||
this.organizationId = params.selectedVault;
|
||||
}
|
||||
|
||||
await this.load();
|
||||
|
||||
if (!this.editMode || this.cloneMode) {
|
||||
// Only allow setting username if there's no existing value
|
||||
if (
|
||||
params.username &&
|
||||
(this.cipher.login.username == null || this.cipher.login.username === "")
|
||||
) {
|
||||
this.cipher.login.username = params.username;
|
||||
}
|
||||
|
||||
if (params.name && (this.cipher.name == null || this.cipher.name === "")) {
|
||||
this.cipher.name = params.name;
|
||||
}
|
||||
if (
|
||||
params.uri &&
|
||||
this.cipher.login.uris[0] &&
|
||||
(this.cipher.login.uris[0].uri == null || this.cipher.login.uris[0].uri === "")
|
||||
) {
|
||||
this.cipher.login.uris[0].uri = params.uri;
|
||||
}
|
||||
}
|
||||
|
||||
this.openAttachmentsInPopup = BrowserPopupUtils.inPopup(window);
|
||||
|
||||
if (this.inAddEditPopoutWindow()) {
|
||||
BrowserApi.messageListener("add-edit-popout", this.handleExtensionMessage.bind(this));
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.editMode) {
|
||||
const tabs = await BrowserApi.tabsQuery({ windowType: "normal" });
|
||||
this.currentUris =
|
||||
tabs == null
|
||||
? null
|
||||
: tabs.filter((tab) => tab.url != null && tab.url !== "").map((tab) => tab.url);
|
||||
}
|
||||
|
||||
this.setFocus();
|
||||
|
||||
if (BrowserPopupUtils.inPopout(window)) {
|
||||
this.popupCloseWarningService.enable();
|
||||
}
|
||||
}
|
||||
|
||||
async load() {
|
||||
await super.load();
|
||||
this.showAutoFillOnPageLoadOptions =
|
||||
this.cipher.type === CipherType.Login &&
|
||||
(await firstValueFrom(this.autofillSettingsService.autofillOnPageLoad$));
|
||||
}
|
||||
|
||||
async submit(): Promise<boolean> {
|
||||
const fido2SessionData = await firstValueFrom(this.fido2PopoutSessionData$);
|
||||
const { isFido2Session, sessionId, userVerification } = fido2SessionData;
|
||||
const inFido2PopoutWindow = BrowserPopupUtils.inPopout(window) && isFido2Session;
|
||||
|
||||
// normalize card expiry year on save
|
||||
if (this.cipher.type === this.cipherType.Card) {
|
||||
this.cipher.card.expYear = normalizeExpiryYearFormat(this.cipher.card.expYear);
|
||||
}
|
||||
|
||||
// TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production.
|
||||
// PM-4577 - https://github.com/bitwarden/clients/pull/8746
|
||||
if (
|
||||
inFido2PopoutWindow &&
|
||||
!(await this.handleFido2UserVerification(sessionId, userVerification))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const success = await super.submit();
|
||||
if (!success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (BrowserPopupUtils.inPopout(window)) {
|
||||
this.popupCloseWarningService.disable();
|
||||
}
|
||||
|
||||
if (inFido2PopoutWindow) {
|
||||
BrowserFido2UserInterfaceSession.confirmNewCredentialResponse(
|
||||
sessionId,
|
||||
this.cipher.id,
|
||||
userVerification,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.inAddEditPopoutWindow()) {
|
||||
this.messagingService.send("addEditCipherSubmitted");
|
||||
await closeAddEditVaultItemPopout(1000);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.cloneMode) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/tabs/vault"]);
|
||||
} else {
|
||||
this.location.back();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
attachments() {
|
||||
super.attachments();
|
||||
|
||||
if (this.openAttachmentsInPopup) {
|
||||
const destinationUrl = this.router
|
||||
.createUrlTree(["/attachments"], { queryParams: { cipherId: this.cipher.id } })
|
||||
.toString();
|
||||
const currentBaseUrl = window.location.href.replace(this.router.url, "");
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
BrowserPopupUtils.openCurrentPagePopout(window, currentBaseUrl + destinationUrl);
|
||||
} else {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/attachments"], { queryParams: { cipherId: this.cipher.id } });
|
||||
}
|
||||
}
|
||||
|
||||
editCollections() {
|
||||
super.editCollections();
|
||||
if (this.cipher.organizationId != null) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/collections"], { queryParams: { cipherId: this.cipher.id } });
|
||||
}
|
||||
}
|
||||
|
||||
async cancel() {
|
||||
super.cancel();
|
||||
|
||||
const sessionData = await firstValueFrom(this.fido2PopoutSessionData$);
|
||||
if (BrowserPopupUtils.inPopout(window) && sessionData.isFido2Session) {
|
||||
this.popupCloseWarningService.disable();
|
||||
BrowserFido2UserInterfaceSession.abortPopout(sessionData.sessionId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.inAddEditPopoutWindow()) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
closeAddEditVaultItemPopout();
|
||||
return;
|
||||
}
|
||||
|
||||
this.location.back();
|
||||
}
|
||||
|
||||
async generateUsername(): Promise<boolean> {
|
||||
const confirmed = await super.generateUsername();
|
||||
if (confirmed) {
|
||||
await this.saveCipherState();
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["generator"], { queryParams: { type: "username" } });
|
||||
}
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
async generatePassword(): Promise<boolean> {
|
||||
const confirmed = await super.generatePassword();
|
||||
if (confirmed) {
|
||||
await this.saveCipherState();
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["generator"], { queryParams: { type: "password" } });
|
||||
}
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
async delete(): Promise<boolean> {
|
||||
const confirmed = await super.delete();
|
||||
if (confirmed) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/tabs/vault"]);
|
||||
}
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
toggleUriInput(uri: LoginUriView) {
|
||||
const u = uri as any;
|
||||
u.showCurrentUris = !u.showCurrentUris;
|
||||
}
|
||||
|
||||
allowOwnershipOptions(): boolean {
|
||||
return (
|
||||
(!this.editMode || this.cloneMode) &&
|
||||
this.ownershipOptions &&
|
||||
(this.ownershipOptions.length > 1 || !this.allowPersonal)
|
||||
);
|
||||
}
|
||||
|
||||
private saveCipherState() {
|
||||
return this.cipherService.setAddEditCipherInfo({
|
||||
cipher: this.cipher,
|
||||
collectionIds:
|
||||
this.collections == null
|
||||
? []
|
||||
: this.collections.filter((c) => (c as any).checked).map((c) => c.id),
|
||||
});
|
||||
}
|
||||
|
||||
private setFocus() {
|
||||
window.setTimeout(() => {
|
||||
if (this.editMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.cipher.name != null && this.cipher.name !== "") {
|
||||
document.getElementById("loginUsername").focus();
|
||||
} else {
|
||||
document.getElementById("name").focus();
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
repromptChanged() {
|
||||
super.repromptChanged();
|
||||
|
||||
if (!this.showAutoFillOnPageLoadOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.reprompt) {
|
||||
this.platformUtilsService.showToast(
|
||||
"info",
|
||||
null,
|
||||
this.i18nService.t("passwordRepromptDisabledAutofillOnPageLoad"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.platformUtilsService.showToast(
|
||||
"info",
|
||||
null,
|
||||
this.i18nService.t("autofillOnPageLoadSetToDefault"),
|
||||
);
|
||||
}
|
||||
|
||||
private inAddEditPopoutWindow() {
|
||||
return BrowserPopupUtils.inSingleActionPopout(window, VaultPopoutType.addEditVaultItem);
|
||||
}
|
||||
|
||||
async captureTOTPFromTab() {
|
||||
try {
|
||||
const screenshot = await BrowserApi.captureVisibleTab();
|
||||
const data = await qrcodeParser(screenshot);
|
||||
const url = new URL(data.toString());
|
||||
if (url.protocol == "otpauth:" && url.searchParams.has("secret")) {
|
||||
this.cipher.login.totp = data.toString();
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("totpCaptureSuccess"),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("totpCaptureError"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private handleExtensionMessage(message: { [key: string]: any; command: string }) {
|
||||
if (message.command === "inlineAutofillMenuRefreshAddEditCipher") {
|
||||
this.load().catch((error) => this.logService.error(error));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Remove and use fido2 user verification service once user verification for passkeys is approved for production.
|
||||
// Be sure to make the same changes to add-edit-v2.component.ts if applicable
|
||||
private async handleFido2UserVerification(
|
||||
sessionId: string,
|
||||
userVerification: boolean,
|
||||
): Promise<boolean> {
|
||||
// We are bypassing user verification pending approval for production.
|
||||
return true;
|
||||
}
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||
<header>
|
||||
<div class="left">
|
||||
<button type="button" (click)="close()" *ngIf="openedAttachmentsInPopup">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
<button type="button" (click)="back()" *ngIf="!openedAttachmentsInPopup">
|
||||
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
|
||||
<span>{{ "back" | i18n }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<h1 class="center">
|
||||
<span class="title">{{ "attachments" | i18n }}</span>
|
||||
</h1>
|
||||
<div class="right">
|
||||
<button type="submit" [disabled]="form.loading">
|
||||
<span [hidden]="form.loading">{{ "save" | i18n }}</span>
|
||||
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<main tabindex="-1">
|
||||
<div class="box" *ngIf="cipher && cipher.hasAttachments">
|
||||
<div class="box-content no-hover single-line">
|
||||
<div class="box-content-row box-content-row-flex" *ngFor="let a of cipher.attachments">
|
||||
<div class="row-main">
|
||||
{{ a.fileName }}
|
||||
</div>
|
||||
<small class="row-sub-label">{{ a.sizeName }}</small>
|
||||
<div class="action-buttons no-pad">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn btn"
|
||||
type="button"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'deleteAttachment' | i18n }}"
|
||||
(click)="delete(a)"
|
||||
#deleteBtn
|
||||
[appApiAction]="deletePromises[a.id]"
|
||||
[disabled]="$any(deleteBtn).loading"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-trash bwi-lg bwi-fw"
|
||||
[hidden]="$any(deleteBtn).loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
|
||||
[hidden]="!$any(deleteBtn).loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<h2 class="box-header">
|
||||
{{ "newAttachment" | i18n }}
|
||||
</h2>
|
||||
<div class="box-content no-hover">
|
||||
<div class="box-content-row">
|
||||
<label for="file">{{ "file" | i18n }}</label>
|
||||
<input type="file" id="file" name="file" aria-describedby="fileHelp" required />
|
||||
</div>
|
||||
</div>
|
||||
<div id="fileHelp" class="box-footer">
|
||||
{{ "maxFileSize" | i18n }}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</form>
|
@ -1,82 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Location } from "@angular/common";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/angular/vault/components/attachments.component";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-attachments",
|
||||
templateUrl: "attachments.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class AttachmentsComponent extends BaseAttachmentsComponent implements OnInit {
|
||||
openedAttachmentsInPopup: boolean;
|
||||
|
||||
constructor(
|
||||
cipherService: CipherService,
|
||||
i18nService: I18nService,
|
||||
keyService: KeyService,
|
||||
encryptService: EncryptService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
apiService: ApiService,
|
||||
private location: Location,
|
||||
private route: ActivatedRoute,
|
||||
stateService: StateService,
|
||||
logService: LogService,
|
||||
fileDownloadService: FileDownloadService,
|
||||
dialogService: DialogService,
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
accountService: AccountService,
|
||||
toastService: ToastService,
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
i18nService,
|
||||
keyService,
|
||||
encryptService,
|
||||
platformUtilsService,
|
||||
apiService,
|
||||
window,
|
||||
logService,
|
||||
stateService,
|
||||
fileDownloadService,
|
||||
dialogService,
|
||||
billingAccountProfileStateService,
|
||||
accountService,
|
||||
toastService,
|
||||
);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.queryParams.pipe(first()).subscribe(async (params) => {
|
||||
this.cipherId = params.cipherId;
|
||||
await this.init();
|
||||
});
|
||||
|
||||
this.openedAttachmentsInPopup = history.length === 1;
|
||||
}
|
||||
|
||||
back() {
|
||||
this.location.back();
|
||||
}
|
||||
|
||||
close() {
|
||||
window.close();
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||
<header>
|
||||
<div class="left">
|
||||
<button type="button" (click)="back()">
|
||||
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
|
||||
<span>{{ "back" | i18n }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<h1 class="center">
|
||||
<span class="title">{{ "collections" | i18n }}</span>
|
||||
</h1>
|
||||
<div class="right">
|
||||
<button type="submit" [disabled]="form.loading">
|
||||
<span [hidden]="form.loading">{{ "save" | i18n }}</span>
|
||||
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<main tabindex="-1">
|
||||
<div class="box">
|
||||
<div class="box-content" *ngIf="!collections || !collections.length">
|
||||
<div class="box-content-row padded no-hover">
|
||||
{{ "noCollectionsInList" | i18n }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content" *ngIf="collections && collections.length">
|
||||
<div
|
||||
class="box-content-row box-content-row-checkbox"
|
||||
*ngFor="let c of collections; let i = index"
|
||||
appBoxRow
|
||||
>
|
||||
<label for="collection_{{ i }}">{{ c.name }}</label>
|
||||
<input
|
||||
id="collection_{{ i }}"
|
||||
type="checkbox"
|
||||
[(ngModel)]="$any(c).checked"
|
||||
name="Collection[{{ i }}].Checked"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</form>
|
@ -1,61 +0,0 @@
|
||||
import { Location } from "@angular/common";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-collections",
|
||||
templateUrl: "collections.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class CollectionsComponent extends BaseCollectionsComponent implements OnInit {
|
||||
constructor(
|
||||
collectionService: CollectionService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
i18nService: I18nService,
|
||||
cipherService: CipherService,
|
||||
organizationService: OrganizationService,
|
||||
private route: ActivatedRoute,
|
||||
private location: Location,
|
||||
logService: LogService,
|
||||
accountService: AccountService,
|
||||
toastService: ToastService,
|
||||
) {
|
||||
super(
|
||||
collectionService,
|
||||
platformUtilsService,
|
||||
i18nService,
|
||||
cipherService,
|
||||
organizationService,
|
||||
logService,
|
||||
accountService,
|
||||
toastService,
|
||||
);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
this.onSavedCollections.subscribe(() => {
|
||||
this.back();
|
||||
});
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.queryParams.pipe(first()).subscribe(async (params) => {
|
||||
this.cipherId = params.cipherId;
|
||||
await this.load();
|
||||
});
|
||||
}
|
||||
|
||||
back() {
|
||||
this.location.back();
|
||||
}
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
<app-header>
|
||||
<h1 class="sr-only">{{ "currentTab" | i18n }}</h1>
|
||||
<div class="left">
|
||||
<app-pop-out *ngIf="!inSidebar"></app-pop-out>
|
||||
<button
|
||||
type="button"
|
||||
(click)="refresh()"
|
||||
appA11yTitle="{{ 'refresh' | i18n }}"
|
||||
*ngIf="inSidebar"
|
||||
>
|
||||
<i class="bwi bwi-refresh-tab bwi-lg bwi-fw" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="search center">
|
||||
<input
|
||||
type="{{ searchTypeSearch ? 'search' : 'text' }}"
|
||||
placeholder="{{ 'searchVault' | i18n }}"
|
||||
id="search"
|
||||
[(ngModel)]="searchText"
|
||||
(input)="search$.next()"
|
||||
autocomplete="off"
|
||||
(keydown)="closeOnEsc($event)"
|
||||
appAutofocus
|
||||
/>
|
||||
<i class="bwi bwi-search" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="right">
|
||||
<button type="button" (click)="addCipher()" appA11yTitle="{{ 'addItem' | i18n }}">
|
||||
<i class="bwi bwi-plus bwi-lg bwi-fw" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</app-header>
|
||||
<main tabindex="-1">
|
||||
<div class="no-items" *ngIf="!loaded">
|
||||
<i class="bwi bwi-spinner bwi-spin bwi-3x" aria-hidden="true"></i>
|
||||
</div>
|
||||
<ng-container *ngIf="loaded">
|
||||
<app-vault-select (onVaultSelectionChanged)="load()"></app-vault-select>
|
||||
<div class="box list" *ngIf="loginCiphers">
|
||||
<h2 class="box-header">
|
||||
{{ "typeLogins" | i18n }}
|
||||
<span class="flex-right">{{ loginCiphers.length }}</span>
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<app-cipher-row
|
||||
*ngFor="let loginCipher of loginCiphers"
|
||||
[cipher]="loginCipher"
|
||||
title="{{ 'autoFill' | i18n }}"
|
||||
[showView]="true"
|
||||
(onSelected)="fillCipher($event)"
|
||||
(onView)="viewCipher($event)"
|
||||
>
|
||||
</app-cipher-row>
|
||||
<div class="box-content-row padded no-hover" *ngIf="!loginCiphers.length">
|
||||
<p class="text-center">{{ "autoFillInfo" | i18n }}</p>
|
||||
<button type="button" class="btn primary link block" (click)="addCipher()">
|
||||
{{ "addLogin" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box list" *ngIf="cardCiphers && cardCiphers.length">
|
||||
<h2 class="box-header">
|
||||
{{ "cards" | i18n }}
|
||||
<span class="flex-right">{{ cardCiphers.length }}</span>
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<app-cipher-row
|
||||
*ngFor="let cardCipher of cardCiphers"
|
||||
[cipher]="cardCipher"
|
||||
title="{{ 'autoFill' | i18n }}"
|
||||
[showView]="true"
|
||||
(onSelected)="fillCipher($event)"
|
||||
(onView)="viewCipher($event)"
|
||||
></app-cipher-row>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box list" *ngIf="identityCiphers && identityCiphers.length">
|
||||
<h2 class="box-header">
|
||||
{{ "identities" | i18n }}
|
||||
<span class="flex-right">{{ identityCiphers.length }}</span>
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<app-cipher-row
|
||||
*ngFor="let identityCipher of identityCiphers"
|
||||
[cipher]="identityCipher"
|
||||
title="{{ 'autoFill' | i18n }}"
|
||||
[showView]="true"
|
||||
(onSelected)="fillCipher($event)"
|
||||
(onView)="viewCipher($event)"
|
||||
></app-cipher-row>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</main>
|
@ -1,354 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { Subject, firstValueFrom, from, Subscription } from "rxjs";
|
||||
import { debounceTime, switchMap, takeUntil } from "rxjs/operators";
|
||||
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
|
||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { AutofillService } from "../../../../autofill/services/abstractions/autofill.service";
|
||||
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils";
|
||||
import { VaultFilterService } from "../../../services/vault-filter.service";
|
||||
|
||||
const BroadcasterSubscriptionId = "CurrentTabComponent";
|
||||
|
||||
@Component({
|
||||
selector: "app-current-tab",
|
||||
templateUrl: "current-tab.component.html",
|
||||
})
|
||||
export class CurrentTabComponent implements OnInit, OnDestroy {
|
||||
pageDetails: any[] = [];
|
||||
tab: chrome.tabs.Tab;
|
||||
cardCiphers: CipherView[];
|
||||
identityCiphers: CipherView[];
|
||||
loginCiphers: CipherView[];
|
||||
url: string;
|
||||
hostname: string;
|
||||
searchText: string;
|
||||
inSidebar = false;
|
||||
searchTypeSearch = false;
|
||||
loaded = false;
|
||||
isLoading = false;
|
||||
showOrganizations = false;
|
||||
showHowToAutofill = false;
|
||||
autofillCalloutText: string;
|
||||
protected search$ = new Subject<void>();
|
||||
private destroy$ = new Subject<void>();
|
||||
private collectPageDetailsSubscription: Subscription;
|
||||
|
||||
private totpCode: string;
|
||||
private totpTimeout: number;
|
||||
private loadedTimeout: number;
|
||||
private searchTimeout: number;
|
||||
|
||||
constructor(
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private cipherService: CipherService,
|
||||
private autofillService: AutofillService,
|
||||
private i18nService: I18nService,
|
||||
private router: Router,
|
||||
private ngZone: NgZone,
|
||||
private broadcasterService: BroadcasterService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private syncService: SyncService,
|
||||
private searchService: SearchService,
|
||||
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
||||
private passwordRepromptService: PasswordRepromptService,
|
||||
private organizationService: OrganizationService,
|
||||
private vaultFilterService: VaultFilterService,
|
||||
private vaultSettingsService: VaultSettingsService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.searchTypeSearch = !this.platformUtilsService.isSafari();
|
||||
this.inSidebar = BrowserPopupUtils.inSidebar(window);
|
||||
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.ngZone.run(async () => {
|
||||
switch (message.command) {
|
||||
case "syncCompleted":
|
||||
if (this.isLoading) {
|
||||
window.setTimeout(() => {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.load();
|
||||
}, 500);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this.changeDetectorRef.detectChanges();
|
||||
});
|
||||
});
|
||||
|
||||
if (!this.syncService.syncInProgress) {
|
||||
await this.load();
|
||||
await this.setCallout();
|
||||
} else {
|
||||
this.loadedTimeout = window.setTimeout(async () => {
|
||||
if (!this.isLoading) {
|
||||
await this.load();
|
||||
await this.setCallout();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
this.search$
|
||||
.pipe(
|
||||
debounceTime(500),
|
||||
switchMap(() => {
|
||||
return from(this.searchVault());
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
const autofillOnPageLoadOrgPolicy = await firstValueFrom(
|
||||
this.autofillSettingsService.activateAutofillOnPageLoadFromPolicy$,
|
||||
);
|
||||
const autofillOnPageLoadPolicyToastHasDisplayed = await firstValueFrom(
|
||||
this.autofillSettingsService.autofillOnPageLoadPolicyToastHasDisplayed$,
|
||||
);
|
||||
|
||||
// If the org "autofill on page load" policy is set, set the user setting to match it
|
||||
// @TODO override user setting instead of overwriting
|
||||
if (autofillOnPageLoadOrgPolicy === true) {
|
||||
await this.autofillSettingsService.setAutofillOnPageLoad(true);
|
||||
|
||||
if (!autofillOnPageLoadPolicyToastHasDisplayed) {
|
||||
this.platformUtilsService.showToast(
|
||||
"info",
|
||||
null,
|
||||
this.i18nService.t("autofillPageLoadPolicyActivated"),
|
||||
);
|
||||
|
||||
await this.autofillSettingsService.setAutofillOnPageLoadPolicyToastHasDisplayed(true);
|
||||
}
|
||||
}
|
||||
|
||||
// If the org policy is ever disabled after being enabled, reset the toast notification
|
||||
if (!autofillOnPageLoadOrgPolicy && autofillOnPageLoadPolicyToastHasDisplayed) {
|
||||
await this.autofillSettingsService.setAutofillOnPageLoadPolicyToastHasDisplayed(false);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
window.clearTimeout(this.loadedTimeout);
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
await this.load();
|
||||
}
|
||||
|
||||
addCipher() {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/add-cipher"], {
|
||||
queryParams: {
|
||||
name: this.hostname,
|
||||
uri: this.url,
|
||||
selectedVault: this.vaultFilterService.getVaultFilter().selectedOrganizationId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
viewCipher(cipher: CipherView) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/view-cipher"], { queryParams: { cipherId: cipher.id } });
|
||||
}
|
||||
|
||||
async fillCipher(cipher: CipherView, closePopupDelay?: number) {
|
||||
if (
|
||||
cipher.reprompt !== CipherRepromptType.None &&
|
||||
!(await this.passwordRepromptService.showPasswordPrompt())
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.totpCode = null;
|
||||
if (this.totpTimeout != null) {
|
||||
window.clearTimeout(this.totpTimeout);
|
||||
}
|
||||
|
||||
if (this.pageDetails == null || this.pageDetails.length === 0) {
|
||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("autofillError"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.totpCode = await this.autofillService.doAutoFill({
|
||||
tab: this.tab,
|
||||
cipher: cipher,
|
||||
pageDetails: this.pageDetails,
|
||||
doc: window.document,
|
||||
fillNewPassword: true,
|
||||
allowTotpAutofill: true,
|
||||
});
|
||||
if (this.totpCode != null) {
|
||||
this.platformUtilsService.copyToClipboard(this.totpCode, { window: window });
|
||||
}
|
||||
if (BrowserPopupUtils.inPopup(window)) {
|
||||
if (!closePopupDelay) {
|
||||
if (this.platformUtilsService.isFirefox() || this.platformUtilsService.isSafari()) {
|
||||
BrowserApi.closePopup(window);
|
||||
} else {
|
||||
// Slight delay to fix bug in Chromium browsers where popup closes without copying totp to clipboard
|
||||
setTimeout(() => BrowserApi.closePopup(window), 50);
|
||||
}
|
||||
} else {
|
||||
setTimeout(() => BrowserApi.closePopup(window), closePopupDelay);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
this.ngZone.run(() => {
|
||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("autofillError"));
|
||||
this.changeDetectorRef.detectChanges();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async searchVault() {
|
||||
if (!(await this.searchService.isSearchable(this.searchText))) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.router.navigate(["/tabs/vault"], { queryParams: { searchText: this.searchText } });
|
||||
}
|
||||
|
||||
closeOnEsc(e: KeyboardEvent) {
|
||||
// If input not empty, use browser default behavior of clearing input instead
|
||||
if (e.key === "Escape" && (this.searchText == null || this.searchText === "")) {
|
||||
BrowserApi.closePopup(window);
|
||||
}
|
||||
}
|
||||
|
||||
protected async load() {
|
||||
this.isLoading = false;
|
||||
this.tab = await BrowserApi.getTabFromCurrentWindow();
|
||||
|
||||
if (this.tab != null) {
|
||||
this.url = this.tab.url;
|
||||
} else {
|
||||
this.loginCiphers = [];
|
||||
this.isLoading = this.loaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.pageDetails = [];
|
||||
this.collectPageDetailsSubscription?.unsubscribe();
|
||||
this.collectPageDetailsSubscription = this.autofillService
|
||||
.collectPageDetailsFromTab$(this.tab)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((pageDetails) => (this.pageDetails = pageDetails));
|
||||
|
||||
this.hostname = Utils.getHostname(this.url);
|
||||
const otherTypes: CipherType[] = [];
|
||||
const dontShowCards = !(await firstValueFrom(this.vaultSettingsService.showCardsCurrentTab$));
|
||||
const dontShowIdentities = !(await firstValueFrom(
|
||||
this.vaultSettingsService.showIdentitiesCurrentTab$,
|
||||
));
|
||||
this.showOrganizations = await this.organizationService.hasOrganizations();
|
||||
if (!dontShowCards) {
|
||||
otherTypes.push(CipherType.Card);
|
||||
}
|
||||
if (!dontShowIdentities) {
|
||||
otherTypes.push(CipherType.Identity);
|
||||
}
|
||||
|
||||
const ciphers = await this.cipherService.getAllDecryptedForUrl(
|
||||
this.url,
|
||||
otherTypes.length > 0 ? otherTypes : null,
|
||||
);
|
||||
|
||||
this.loginCiphers = [];
|
||||
this.cardCiphers = [];
|
||||
this.identityCiphers = [];
|
||||
|
||||
ciphers.forEach((c) => {
|
||||
if (!this.vaultFilterService.filterCipherForSelectedVault(c)) {
|
||||
switch (c.type) {
|
||||
case CipherType.Login:
|
||||
this.loginCiphers.push(c);
|
||||
break;
|
||||
case CipherType.Card:
|
||||
this.cardCiphers.push(c);
|
||||
break;
|
||||
case CipherType.Identity:
|
||||
this.identityCiphers.push(c);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (this.loginCiphers.length) {
|
||||
this.loginCiphers = this.loginCiphers.sort((a, b) =>
|
||||
this.cipherService.sortCiphersByLastUsedThenName(a, b),
|
||||
);
|
||||
}
|
||||
|
||||
this.isLoading = this.loaded = true;
|
||||
}
|
||||
|
||||
async goToSettings() {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["autofill"]);
|
||||
}
|
||||
|
||||
async dismissCallout() {
|
||||
await this.autofillSettingsService.setAutofillOnPageLoadCalloutIsDismissed(true);
|
||||
this.showHowToAutofill = false;
|
||||
}
|
||||
|
||||
private async setCallout() {
|
||||
const inlineMenuVisibilityIsOff =
|
||||
(await firstValueFrom(this.autofillSettingsService.inlineMenuVisibility$)) ===
|
||||
AutofillOverlayVisibility.Off;
|
||||
|
||||
this.showHowToAutofill =
|
||||
this.loginCiphers.length > 0 &&
|
||||
inlineMenuVisibilityIsOff &&
|
||||
!(await firstValueFrom(this.autofillSettingsService.autofillOnPageLoad$)) &&
|
||||
!(await firstValueFrom(this.autofillSettingsService.autofillOnPageLoadCalloutIsDismissed$));
|
||||
|
||||
if (this.showHowToAutofill) {
|
||||
const autofillCommand = await this.platformUtilsService.getAutofillKeyboardShortcut();
|
||||
await this.setAutofillCalloutText(autofillCommand);
|
||||
}
|
||||
}
|
||||
|
||||
private setAutofillCalloutText(command: string) {
|
||||
if (command) {
|
||||
this.autofillCalloutText = this.i18nService.t("autofillSelectInfoWithCommand", command);
|
||||
} else {
|
||||
this.autofillCalloutText = this.i18nService.t("autofillSelectInfoWithoutCommand");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
<header>
|
||||
<div class="left">
|
||||
<button type="button" (click)="close()">{{ "close" | i18n }}</button>
|
||||
</div>
|
||||
<h1 class="center">
|
||||
<span class="title">{{ "passwordHistory" | i18n }}</span>
|
||||
</h1>
|
||||
<div class="right"></div>
|
||||
</header>
|
||||
<main tabindex="-1">
|
||||
<div class="box list full-list" *ngIf="history && history.length">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row box-content-row-flex" *ngFor="let h of history">
|
||||
<div class="row-main">
|
||||
<div class="row-main-content">
|
||||
<span
|
||||
class="text monospaced no-ellipsis"
|
||||
[innerHTML]="h.password | colorPassword"
|
||||
></span>
|
||||
<span class="detail">{{ h.lastUsedDate | date: "medium" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'copyPassword' | i18n }}"
|
||||
(click)="copy(h.password)"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="no-items" *ngIf="!history || !history.length">
|
||||
<p>{{ "noPasswordsInList" | i18n }}</p>
|
||||
</div>
|
||||
</main>
|
@ -1,44 +0,0 @@
|
||||
import { Location } from "@angular/common";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { PasswordHistoryComponent as BasePasswordHistoryComponent } from "@bitwarden/angular/vault/components/password-history.component";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-password-history",
|
||||
templateUrl: "password-history.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class PasswordHistoryComponent extends BasePasswordHistoryComponent implements OnInit {
|
||||
constructor(
|
||||
cipherService: CipherService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
i18nService: I18nService,
|
||||
accountService: AccountService,
|
||||
private location: Location,
|
||||
private route: ActivatedRoute,
|
||||
) {
|
||||
super(cipherService, platformUtilsService, i18nService, accountService, window);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.queryParams.pipe(first()).subscribe(async (params) => {
|
||||
if (params.cipherId) {
|
||||
this.cipherId = params.cipherId;
|
||||
} else {
|
||||
this.close();
|
||||
}
|
||||
await this.init();
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.location.back();
|
||||
}
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||
<ng-container *ngIf="organizations$ | async as organizations">
|
||||
<header>
|
||||
<div class="left">
|
||||
<button type="button" (click)="cancel()">{{ "cancel" | i18n }}</button>
|
||||
</div>
|
||||
<h1 class="center">
|
||||
<span class="title">{{ "moveToOrganization" | i18n }}</span>
|
||||
</h1>
|
||||
<div class="right">
|
||||
<button
|
||||
type="submit"
|
||||
[disabled]="form.loading || !canSave"
|
||||
*ngIf="organizations && organizations.length"
|
||||
>
|
||||
<span [hidden]="form.loading">{{ "move" | i18n }}</span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-lg bwi-spin"
|
||||
[hidden]="!form.loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<main tabindex="-1">
|
||||
<div class="box">
|
||||
<div class="box-content" *ngIf="!organizations || !organizations.length">
|
||||
<div class="box-content-row padded no-hover">
|
||||
{{ "noOrganizationsList" | i18n }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content" *ngIf="organizations && organizations.length">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="organization">{{ "organization" | i18n }}</label>
|
||||
<select
|
||||
id="organization"
|
||||
name="OrganizationId"
|
||||
aria-describedby="organizationHelp"
|
||||
[(ngModel)]="organizationId"
|
||||
(change)="filterCollections()"
|
||||
>
|
||||
<option *ngFor="let o of organizations" [ngValue]="o.id">{{ o.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="organizationHelp" class="box-footer">
|
||||
{{ "moveToOrgDesc" | i18n }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" *ngIf="organizations && organizations.length">
|
||||
<h2 class="box-header">
|
||||
{{ "collections" | i18n }}
|
||||
</h2>
|
||||
<div class="box-content" *ngIf="!collections || !collections.length">
|
||||
<div class="box-content-row padded no-hover">
|
||||
{{ "noCollectionsInList" | i18n }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content" *ngIf="collections && collections.length">
|
||||
<div
|
||||
class="box-content-row box-content-row-checkbox"
|
||||
*ngFor="let c of collections; let i = index"
|
||||
appBoxRow
|
||||
>
|
||||
<label for="collection_{{ i }}">{{ c.name }}</label>
|
||||
<input
|
||||
id="collection_{{ i }}"
|
||||
type="checkbox"
|
||||
[(ngModel)]="c.checked"
|
||||
name="Collection[{{ i }}].Checked"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</ng-container>
|
||||
</form>
|
@ -1,72 +0,0 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { ShareComponent as BaseShareComponent } from "@bitwarden/angular/components/share.component";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-share",
|
||||
templateUrl: "share.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class ShareComponent extends BaseShareComponent implements OnInit {
|
||||
constructor(
|
||||
collectionService: CollectionService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
i18nService: I18nService,
|
||||
logService: LogService,
|
||||
cipherService: CipherService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
organizationService: OrganizationService,
|
||||
accountService: AccountService,
|
||||
) {
|
||||
super(
|
||||
collectionService,
|
||||
platformUtilsService,
|
||||
i18nService,
|
||||
cipherService,
|
||||
logService,
|
||||
organizationService,
|
||||
accountService,
|
||||
);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
this.onSharedCipher.subscribe(() => {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["view-cipher", { cipherId: this.cipherId }]);
|
||||
});
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.queryParams.pipe(first()).subscribe(async (params) => {
|
||||
this.cipherId = params.cipherId;
|
||||
await this.load();
|
||||
});
|
||||
}
|
||||
|
||||
async submit(): Promise<boolean> {
|
||||
const success = await super.submit();
|
||||
if (success) {
|
||||
this.cancel();
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
cancel() {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/view-cipher"], {
|
||||
replaceUrl: true,
|
||||
queryParams: { cipherId: this.cipher.id },
|
||||
});
|
||||
}
|
||||
}
|
@ -1,238 +0,0 @@
|
||||
<app-header>
|
||||
<div class="left">
|
||||
<app-pop-out></app-pop-out>
|
||||
</div>
|
||||
<h1 class="sr-only">{{ "myVault" | i18n }}</h1>
|
||||
<div class="search center">
|
||||
<input
|
||||
type="{{ searchTypeSearch ? 'search' : 'text' }}"
|
||||
placeholder="{{ 'searchVault' | i18n }}"
|
||||
id="search"
|
||||
[(ngModel)]="searchText"
|
||||
(input)="search(200)"
|
||||
autocomplete="off"
|
||||
appAutofocus
|
||||
(keydown)="closeOnEsc($event)"
|
||||
/>
|
||||
<i class="bwi bwi-search"></i>
|
||||
</div>
|
||||
<div class="right">
|
||||
<button type="button" (click)="addCipher()" appA11yTitle="{{ 'addItem' | i18n }}">
|
||||
<i class="bwi bwi-plus bwi-lg bwi-fw" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</app-header>
|
||||
<main tabindex="-1" cdk-scrollable>
|
||||
<app-vault-select
|
||||
(onVaultSelectionChanged)="vaultFilterChanged()"
|
||||
class="select-index-top"
|
||||
></app-vault-select>
|
||||
<div class="no-items" *ngIf="(!ciphers || !ciphers.length) && !showSearching()">
|
||||
<i class="bwi bwi-spinner bwi-spin bwi-3x" *ngIf="!loaded"></i>
|
||||
<ng-container *ngIf="loaded">
|
||||
<img class="no-items-image" aria-hidden="true" />
|
||||
<p>{{ "noItemsInList" | i18n }}</p>
|
||||
<button type="button" (click)="addCipher()" class="btn block primary link">
|
||||
{{ "addItem" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
<ng-container *ngIf="ciphers && ciphers.length && !showSearching()">
|
||||
<div class="box list" *ngIf="favoriteCiphers">
|
||||
<h2 class="box-header">
|
||||
{{ "favorites" | i18n }}
|
||||
<span class="flex-right">{{ favoriteCiphers.length }}</span>
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<app-cipher-row
|
||||
*ngFor="let favoriteCipher of favoriteCiphers"
|
||||
[cipher]="favoriteCipher"
|
||||
title="{{ 'viewItem' | i18n }}"
|
||||
(onSelected)="selectCipher($event)"
|
||||
(launchEvent)="launchCipher($event)"
|
||||
>
|
||||
</app-cipher-row>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box list">
|
||||
<h2 class="box-header">
|
||||
{{ "types" | i18n }}
|
||||
<span class="flex-right">4</span>
|
||||
</h2>
|
||||
<div class="box-content single-line">
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row"
|
||||
appStopClick
|
||||
(click)="selectType(cipherType.Login)"
|
||||
>
|
||||
<div class="row-main">
|
||||
<div class="icon"><i class="bwi bwi-fw bwi-lg bwi-globe"></i></div>
|
||||
<span class="text">{{ "typeLogin" | i18n }}</span>
|
||||
</div>
|
||||
<span class="row-sub-label">
|
||||
{{ typeCounts.get(cipherType.Login) || 0 }}
|
||||
</span>
|
||||
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row"
|
||||
appStopClick
|
||||
(click)="selectType(cipherType.Card)"
|
||||
>
|
||||
<div class="row-main">
|
||||
<div class="icon"><i class="bwi bwi-fw bwi-lg bwi-credit-card"></i></div>
|
||||
<span class="text">{{ "typeCard" | i18n }}</span>
|
||||
</div>
|
||||
<span class="row-sub-label">{{ typeCounts.get(cipherType.Card) || 0 }}</span>
|
||||
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row"
|
||||
appStopClick
|
||||
(click)="selectType(cipherType.Identity)"
|
||||
>
|
||||
<div class="row-main">
|
||||
<div class="icon"><i class="bwi bwi-fw bwi-lg bwi-id-card"></i></div>
|
||||
<span class="text">{{ "typeIdentity" | i18n }}</span>
|
||||
</div>
|
||||
<span class="row-sub-label">{{ typeCounts.get(cipherType.Identity) || 0 }}</span>
|
||||
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row"
|
||||
appStopClick
|
||||
(click)="selectType(cipherType.SecureNote)"
|
||||
>
|
||||
<div class="row-main">
|
||||
<div class="icon"><i class="bwi bwi-fw bwi-lg bwi-sticky-note"></i></div>
|
||||
<span class="text">{{ "typeSecureNote" | i18n }}</span>
|
||||
</div>
|
||||
<span class="row-sub-label">{{ typeCounts.get(cipherType.SecureNote) || 0 }}</span>
|
||||
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row"
|
||||
appStopClick
|
||||
*ngIf="isSshKeysEnabled"
|
||||
(click)="selectType(cipherType.SshKey)"
|
||||
>
|
||||
<div class="row-main">
|
||||
<div class="icon"><i class="bwi bwi-fw bwi-lg bwi-key"></i></div>
|
||||
<span class="text">{{ "typeSshKey" | i18n }}</span>
|
||||
</div>
|
||||
<span class="row-sub-label">{{ typeCounts.get(cipherType.SshKey) || 0 }}</span>
|
||||
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box list" *ngIf="nestedFolders?.length">
|
||||
<h2 class="box-header">
|
||||
{{ "folders" | i18n }}
|
||||
<span class="flex-right">{{ folderCount }}</span>
|
||||
</h2>
|
||||
<div class="box-content single-line">
|
||||
<button
|
||||
type="button"
|
||||
*ngFor="let f of nestedFolders"
|
||||
class="box-content-row"
|
||||
appStopClick
|
||||
(click)="selectFolder(f.node)"
|
||||
>
|
||||
<div class="row-main">
|
||||
<div class="icon">
|
||||
<i class="bwi bwi-fw bwi-lg bwi-folder"></i>
|
||||
</div>
|
||||
<span class="text">{{ f.node.name }}</span>
|
||||
</div>
|
||||
<span class="row-sub-label">{{ folderCounts.get(f.node.id) || 0 }}</span>
|
||||
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box list" *ngIf="showCollections && nestedCollections && nestedCollections.length">
|
||||
<h2 class="box-header">
|
||||
{{ "collections" | i18n }}
|
||||
<span class="flex-right">{{ nestedCollections.length }}</span>
|
||||
</h2>
|
||||
<div class="box-content single-line">
|
||||
<button
|
||||
type="button"
|
||||
*ngFor="let nestedCollection of nestedCollections"
|
||||
class="box-content-row"
|
||||
appStopClick
|
||||
(click)="selectCollection(nestedCollection.node)"
|
||||
>
|
||||
<div class="row-main">
|
||||
<div class="icon"><i class="bwi bwi-fw bwi-lg bwi-collection"></i></div>
|
||||
<span class="text">{{ nestedCollection.node.name }}</span>
|
||||
</div>
|
||||
<span class="row-sub-label">{{
|
||||
collectionCounts.get(nestedCollection.node.id) || 0
|
||||
}}</span>
|
||||
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box list" *ngIf="showNoFolderCiphers">
|
||||
<h2 class="box-header">
|
||||
{{ "noneFolder" | i18n }}
|
||||
<div class="flex-right">{{ noFolderCiphers.length }}</div>
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<app-cipher-row
|
||||
*ngFor="let noFolderCipher of noFolderCiphers"
|
||||
[cipher]="noFolderCipher"
|
||||
title="{{ 'viewItem' | i18n }}"
|
||||
(onSelected)="selectCipher($event)"
|
||||
(launchEvent)="launchCipher($event)"
|
||||
>
|
||||
</app-cipher-row>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box list" *ngIf="deletedCount">
|
||||
<h2 class="box-header">
|
||||
{{ "trash" | i18n }}
|
||||
<span class="flex-right">{{ deletedCount }}</span>
|
||||
</h2>
|
||||
<div class="box-content single-line">
|
||||
<button type="button" class="box-content-row" appStopClick (click)="selectTrash()">
|
||||
<div class="row-main">
|
||||
<div class="icon"><i class="bwi bwi-fw bwi-lg bwi-trash"></i></div>
|
||||
<span class="text">{{ "trash" | i18n }}</span>
|
||||
</div>
|
||||
<span class="row-sub-label">{{ deletedCount }}</span>
|
||||
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="showSearching()">
|
||||
<div class="no-items" *ngIf="!ciphers || !ciphers.length">
|
||||
<p>{{ "noItemsInList" | i18n }}</p>
|
||||
</div>
|
||||
<cdk-virtual-scroll-viewport
|
||||
itemSize="55"
|
||||
minBufferPx="400"
|
||||
maxBufferPx="600"
|
||||
*ngIf="ciphers && ciphers.length > 0"
|
||||
>
|
||||
<div class="box list full-list">
|
||||
<div class="box-content">
|
||||
<app-cipher-row
|
||||
*cdkVirtualFor="let searchedCipher of ciphers"
|
||||
[cipher]="searchedCipher"
|
||||
title="{{ 'viewItem' | i18n }}"
|
||||
(onSelected)="selectCipher($event)"
|
||||
(launchEvent)="launchCipher($event)"
|
||||
>
|
||||
</app-cipher-row>
|
||||
</div>
|
||||
</div>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
</ng-container>
|
||||
</main>
|
@ -1,482 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Location } from "@angular/common";
|
||||
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { BehaviorSubject, Subject, firstValueFrom, from } from "rxjs";
|
||||
import { first, switchMap, takeUntil } from "rxjs/operators";
|
||||
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
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";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
import { BrowserGroupingsComponentState } from "../../../../models/browserGroupingsComponentState";
|
||||
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils";
|
||||
import { VaultBrowserStateService } from "../../../services/vault-browser-state.service";
|
||||
import { VaultFilterService } from "../../../services/vault-filter.service";
|
||||
|
||||
const ComponentId = "VaultComponent";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-filter",
|
||||
templateUrl: "vault-filter.component.html",
|
||||
})
|
||||
export class VaultFilterComponent implements OnInit, OnDestroy {
|
||||
get showNoFolderCiphers(): boolean {
|
||||
return (
|
||||
this.noFolderCiphers != null &&
|
||||
this.noFolderCiphers.length < this.noFolderListSize &&
|
||||
this.collections.length === 0
|
||||
);
|
||||
}
|
||||
|
||||
get folderCount(): number {
|
||||
return this.nestedFolders.length - (this.showNoFolderCiphers ? 0 : 1);
|
||||
}
|
||||
folders: FolderView[];
|
||||
nestedFolders: TreeNode<FolderView>[];
|
||||
collections: CollectionView[];
|
||||
nestedCollections: TreeNode<CollectionView>[];
|
||||
loaded = false;
|
||||
cipherType = CipherType;
|
||||
ciphers: CipherView[];
|
||||
favoriteCiphers: CipherView[];
|
||||
noFolderCiphers: CipherView[];
|
||||
folderCounts = new Map<string, number>();
|
||||
collectionCounts = new Map<string, number>();
|
||||
typeCounts = new Map<CipherType, number>();
|
||||
state: BrowserGroupingsComponentState;
|
||||
showLeftHeader = true;
|
||||
searchPending = false;
|
||||
searchTypeSearch = false;
|
||||
deletedCount = 0;
|
||||
vaultFilter: VaultFilter;
|
||||
selectedOrganization: string = null;
|
||||
showCollections = true;
|
||||
|
||||
isSshKeysEnabled = false;
|
||||
|
||||
private loadedTimeout: number;
|
||||
private selectedTimeout: number;
|
||||
private preventSelected = false;
|
||||
private noFolderListSize = 100;
|
||||
private searchTimeout: any = null;
|
||||
private hasSearched = false;
|
||||
private hasLoadedAllCiphers = false;
|
||||
private allCiphers: CipherView[] = null;
|
||||
private destroy$ = new Subject<void>();
|
||||
private _searchText$ = new BehaviorSubject<string>("");
|
||||
private isSearchable: boolean = false;
|
||||
|
||||
get searchText() {
|
||||
return this._searchText$.value;
|
||||
}
|
||||
set searchText(value: string) {
|
||||
this._searchText$.next(value);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private cipherService: CipherService,
|
||||
private router: Router,
|
||||
private ngZone: NgZone,
|
||||
private broadcasterService: BroadcasterService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private route: ActivatedRoute,
|
||||
private syncService: SyncService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private searchService: SearchService,
|
||||
private location: Location,
|
||||
private vaultFilterService: VaultFilterService,
|
||||
private vaultBrowserStateService: VaultBrowserStateService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
this.noFolderListSize = 100;
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.searchTypeSearch = !this.platformUtilsService.isSafari();
|
||||
this.showLeftHeader = !(
|
||||
BrowserPopupUtils.inSidebar(window) && this.platformUtilsService.isFirefox()
|
||||
);
|
||||
await this.vaultBrowserStateService.setBrowserVaultItemsComponentState(null);
|
||||
|
||||
this.broadcasterService.subscribe(ComponentId, (message: any) => {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.ngZone.run(async () => {
|
||||
switch (message.command) {
|
||||
case "syncCompleted":
|
||||
window.setTimeout(() => {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.load();
|
||||
}, 500);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this.changeDetectorRef.detectChanges();
|
||||
});
|
||||
});
|
||||
|
||||
const restoredScopeState = await this.restoreState();
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.queryParams.pipe(first()).subscribe(async (params) => {
|
||||
this.state = await this.vaultBrowserStateService.getBrowserGroupingsComponentState();
|
||||
if (this.state?.searchText) {
|
||||
this.searchText = this.state.searchText;
|
||||
} else if (params.searchText) {
|
||||
this.searchText = params.searchText;
|
||||
this.location.replaceState("vault");
|
||||
}
|
||||
|
||||
if (!this.syncService.syncInProgress) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.load();
|
||||
} else {
|
||||
this.loadedTimeout = window.setTimeout(() => {
|
||||
if (!this.loaded) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.load();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
if (!this.syncService.syncInProgress || restoredScopeState) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
BrowserPopupUtils.setContentScrollY(window, this.state?.scrollY);
|
||||
}
|
||||
});
|
||||
|
||||
this._searchText$
|
||||
.pipe(
|
||||
switchMap((searchText) => from(this.searchService.isSearchable(searchText))),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((isSearchable) => {
|
||||
this.isSearchable = isSearchable;
|
||||
});
|
||||
|
||||
this.isSshKeysEnabled = await this.configService.getFeatureFlag(FeatureFlag.SSHKeyVaultItem);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.loadedTimeout != null) {
|
||||
window.clearTimeout(this.loadedTimeout);
|
||||
}
|
||||
if (this.selectedTimeout != null) {
|
||||
window.clearTimeout(this.selectedTimeout);
|
||||
}
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.saveState();
|
||||
this.broadcasterService.unsubscribe(ComponentId);
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.vaultFilter = this.vaultFilterService.getVaultFilter();
|
||||
|
||||
this.updateSelectedOrg();
|
||||
await this.loadCollectionsAndFolders();
|
||||
await this.loadCiphers();
|
||||
|
||||
if (this.showNoFolderCiphers && this.nestedFolders.length > 0) {
|
||||
// Remove "No Folder" from folder listing
|
||||
this.nestedFolders = this.nestedFolders.slice(0, this.nestedFolders.length - 1);
|
||||
}
|
||||
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
async loadCiphers() {
|
||||
this.allCiphers = await this.cipherService.getAllDecrypted();
|
||||
if (!this.hasLoadedAllCiphers) {
|
||||
this.hasLoadedAllCiphers = !(await this.searchService.isSearchable(this.searchText));
|
||||
}
|
||||
await this.search(null);
|
||||
this.getCounts();
|
||||
}
|
||||
|
||||
async loadCollections() {
|
||||
const allCollections = await this.vaultFilterService.buildCollections(
|
||||
this.selectedOrganization,
|
||||
);
|
||||
this.collections = allCollections.fullList;
|
||||
this.nestedCollections = allCollections.nestedList;
|
||||
}
|
||||
|
||||
async loadFolders() {
|
||||
const allFolders = await firstValueFrom(
|
||||
this.vaultFilterService.buildNestedFolders(this.selectedOrganization),
|
||||
);
|
||||
this.folders = allFolders.fullList;
|
||||
this.nestedFolders = allFolders.nestedList;
|
||||
}
|
||||
|
||||
async search(timeout: number = null) {
|
||||
this.searchPending = false;
|
||||
if (this.searchTimeout != null) {
|
||||
clearTimeout(this.searchTimeout);
|
||||
}
|
||||
const filterDeleted = (c: CipherView) => !c.isDeleted;
|
||||
if (timeout == null) {
|
||||
this.hasSearched = this.isSearchable;
|
||||
this.ciphers = await this.searchService.searchCiphers(
|
||||
this.searchText,
|
||||
filterDeleted,
|
||||
this.allCiphers,
|
||||
);
|
||||
this.ciphers = this.ciphers.filter(
|
||||
(c) => !this.vaultFilterService.filterCipherForSelectedVault(c),
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.searchPending = true;
|
||||
this.searchTimeout = setTimeout(async () => {
|
||||
this.hasSearched = this.isSearchable;
|
||||
if (!this.hasLoadedAllCiphers && !this.hasSearched) {
|
||||
await this.loadCiphers();
|
||||
} else {
|
||||
this.ciphers = await this.searchService.searchCiphers(
|
||||
this.searchText,
|
||||
filterDeleted,
|
||||
this.allCiphers,
|
||||
);
|
||||
}
|
||||
this.ciphers = this.ciphers.filter(
|
||||
(c) => !this.vaultFilterService.filterCipherForSelectedVault(c),
|
||||
);
|
||||
this.searchPending = false;
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
async selectType(type: CipherType) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/ciphers"], { queryParams: { type: type } });
|
||||
}
|
||||
|
||||
async selectFolder(folder: FolderView) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/ciphers"], { queryParams: { folderId: folder.id || "none" } });
|
||||
}
|
||||
|
||||
async selectCollection(collection: CollectionView) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/ciphers"], { queryParams: { collectionId: collection.id } });
|
||||
}
|
||||
|
||||
async selectTrash() {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/ciphers"], { queryParams: { deleted: true } });
|
||||
}
|
||||
|
||||
async selectCipher(cipher: CipherView) {
|
||||
this.selectedTimeout = window.setTimeout(() => {
|
||||
if (!this.preventSelected) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/view-cipher"], { queryParams: { cipherId: cipher.id } });
|
||||
}
|
||||
this.preventSelected = false;
|
||||
}, 200);
|
||||
}
|
||||
|
||||
async launchCipher(cipher: CipherView) {
|
||||
if (cipher.type !== CipherType.Login || !cipher.login.canLaunch) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.selectedTimeout != null) {
|
||||
window.clearTimeout(this.selectedTimeout);
|
||||
}
|
||||
this.preventSelected = true;
|
||||
await this.cipherService.updateLastLaunchedDate(cipher.id);
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
BrowserApi.createNewTab(cipher.login.launchUri);
|
||||
if (BrowserPopupUtils.inPopup(window)) {
|
||||
BrowserApi.closePopup(window);
|
||||
}
|
||||
}
|
||||
|
||||
async addCipher() {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/add-cipher"], {
|
||||
queryParams: { selectedVault: this.vaultFilter.selectedOrganizationId },
|
||||
});
|
||||
}
|
||||
|
||||
async vaultFilterChanged() {
|
||||
if (this.showSearching) {
|
||||
await this.search();
|
||||
}
|
||||
this.updateSelectedOrg();
|
||||
await this.loadCollectionsAndFolders();
|
||||
this.getCounts();
|
||||
}
|
||||
|
||||
updateSelectedOrg() {
|
||||
this.vaultFilter = this.vaultFilterService.getVaultFilter();
|
||||
if (this.vaultFilter.selectedOrganizationId != null) {
|
||||
this.selectedOrganization = this.vaultFilter.selectedOrganizationId;
|
||||
} else {
|
||||
this.selectedOrganization = null;
|
||||
}
|
||||
}
|
||||
|
||||
getCounts() {
|
||||
let favoriteCiphers: CipherView[] = null;
|
||||
let noFolderCiphers: CipherView[] = null;
|
||||
const folderCounts = new Map<string, number>();
|
||||
const collectionCounts = new Map<string, number>();
|
||||
const typeCounts = new Map<CipherType, number>();
|
||||
|
||||
this.deletedCount = this.allCiphers.filter(
|
||||
(c) => c.isDeleted && !this.vaultFilterService.filterCipherForSelectedVault(c),
|
||||
).length;
|
||||
|
||||
this.ciphers?.forEach((c) => {
|
||||
if (!this.vaultFilterService.filterCipherForSelectedVault(c)) {
|
||||
if (c.isDeleted) {
|
||||
return;
|
||||
}
|
||||
if (c.favorite) {
|
||||
if (favoriteCiphers == null) {
|
||||
favoriteCiphers = [];
|
||||
}
|
||||
favoriteCiphers.push(c);
|
||||
}
|
||||
|
||||
if (c.folderId == null) {
|
||||
if (noFolderCiphers == null) {
|
||||
noFolderCiphers = [];
|
||||
}
|
||||
noFolderCiphers.push(c);
|
||||
}
|
||||
|
||||
if (typeCounts.has(c.type)) {
|
||||
typeCounts.set(c.type, typeCounts.get(c.type) + 1);
|
||||
} else {
|
||||
typeCounts.set(c.type, 1);
|
||||
}
|
||||
|
||||
if (folderCounts.has(c.folderId)) {
|
||||
folderCounts.set(c.folderId, folderCounts.get(c.folderId) + 1);
|
||||
} else {
|
||||
folderCounts.set(c.folderId, 1);
|
||||
}
|
||||
|
||||
if (c.collectionIds != null) {
|
||||
c.collectionIds.forEach((colId) => {
|
||||
if (collectionCounts.has(colId)) {
|
||||
collectionCounts.set(colId, collectionCounts.get(colId) + 1);
|
||||
} else {
|
||||
collectionCounts.set(colId, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.favoriteCiphers = favoriteCiphers;
|
||||
this.noFolderCiphers = noFolderCiphers;
|
||||
this.typeCounts = typeCounts;
|
||||
this.folderCounts = folderCounts;
|
||||
this.collectionCounts = collectionCounts;
|
||||
}
|
||||
|
||||
showSearching() {
|
||||
return this.hasSearched || (!this.searchPending && this.isSearchable);
|
||||
}
|
||||
|
||||
closeOnEsc(e: KeyboardEvent) {
|
||||
// If input not empty, use browser default behavior of clearing input instead
|
||||
if (e.key === "Escape" && (this.searchText == null || this.searchText === "")) {
|
||||
BrowserApi.closePopup(window);
|
||||
}
|
||||
}
|
||||
|
||||
private async loadCollectionsAndFolders() {
|
||||
this.showCollections = !this.vaultFilter.myVaultOnly;
|
||||
await this.loadFolders();
|
||||
await this.loadCollections();
|
||||
}
|
||||
|
||||
private async saveState() {
|
||||
this.state = Object.assign(new BrowserGroupingsComponentState(), {
|
||||
scrollY: BrowserPopupUtils.getContentScrollY(window),
|
||||
searchText: this.searchText,
|
||||
favoriteCiphers: this.favoriteCiphers,
|
||||
noFolderCiphers: this.noFolderCiphers,
|
||||
ciphers: this.ciphers,
|
||||
collectionCounts: this.collectionCounts,
|
||||
folderCounts: this.folderCounts,
|
||||
typeCounts: this.typeCounts,
|
||||
folders: this.folders,
|
||||
collections: this.collections,
|
||||
deletedCount: this.deletedCount,
|
||||
});
|
||||
await this.vaultBrowserStateService.setBrowserGroupingsComponentState(this.state);
|
||||
}
|
||||
|
||||
private async restoreState(): Promise<boolean> {
|
||||
this.state = await this.vaultBrowserStateService.getBrowserGroupingsComponentState();
|
||||
if (this.state == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.state.favoriteCiphers != null) {
|
||||
this.favoriteCiphers = this.state.favoriteCiphers;
|
||||
}
|
||||
if (this.state.noFolderCiphers != null) {
|
||||
this.noFolderCiphers = this.state.noFolderCiphers;
|
||||
}
|
||||
if (this.state.ciphers != null) {
|
||||
this.ciphers = this.state.ciphers;
|
||||
}
|
||||
if (this.state.collectionCounts != null) {
|
||||
this.collectionCounts = this.state.collectionCounts;
|
||||
}
|
||||
if (this.state.folderCounts != null) {
|
||||
this.folderCounts = this.state.folderCounts;
|
||||
}
|
||||
if (this.state.typeCounts != null) {
|
||||
this.typeCounts = this.state.typeCounts;
|
||||
}
|
||||
if (this.state.folders != null) {
|
||||
this.folders = this.state.folders;
|
||||
}
|
||||
if (this.state.collections != null) {
|
||||
this.collections = this.state.collections;
|
||||
}
|
||||
if (this.state.deletedCount != null) {
|
||||
this.deletedCount = this.state.deletedCount;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@ -1,123 +0,0 @@
|
||||
<header>
|
||||
<div class="left">
|
||||
<button type="button" (click)="back()">
|
||||
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
|
||||
<span>{{ "back" | i18n }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<h1 class="sr-only">{{ "myVault" | i18n }}</h1>
|
||||
<div class="search">
|
||||
<input
|
||||
type="{{ searchTypeSearch ? 'search' : 'text' }}"
|
||||
placeholder="{{ searchPlaceholder || ('searchVault' | i18n) }}"
|
||||
id="search"
|
||||
[(ngModel)]="searchText"
|
||||
(input)="search(200)"
|
||||
autocomplete="off"
|
||||
appAutofocus
|
||||
/>
|
||||
<i class="bwi bwi-search" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="right">
|
||||
<button type="button" (click)="addCipher()" appA11yTitle="{{ 'addItem' | i18n }}">
|
||||
<i class="bwi bwi-plus bwi-lg bwi-fw" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<main tabindex="-1" [ngClass]="{ 'stacked-boxes': showGroupings() }">
|
||||
<ng-container *ngIf="showGroupings()">
|
||||
<app-vault-select
|
||||
*ngIf="showVaultFilter"
|
||||
(onVaultSelectionChanged)="changeVaultSelection()"
|
||||
></app-vault-select>
|
||||
<div class="box list" *ngIf="nestedFolders && nestedFolders.length">
|
||||
<h2 class="box-header">
|
||||
{{ "folders" | i18n }}
|
||||
</h2>
|
||||
<div class="box-content single-line">
|
||||
<button
|
||||
type="button"
|
||||
*ngFor="let f of nestedFolders"
|
||||
class="box-content-row"
|
||||
appStopClick
|
||||
(click)="selectFolder(f.node)"
|
||||
>
|
||||
<div class="row-main">
|
||||
<div class="icon">
|
||||
<i class="bwi bwi-fw bwi-lg bwi-folder" aria-hidden="true"></i>
|
||||
</div>
|
||||
<span class="text">{{ f.node.name }}</span>
|
||||
</div>
|
||||
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box list" *ngIf="nestedCollections && nestedCollections.length">
|
||||
<h2 class="box-header">
|
||||
{{ "collections" | i18n }}
|
||||
</h2>
|
||||
<div class="box-content single-line">
|
||||
<button
|
||||
type="button"
|
||||
*ngFor="let c of nestedCollections"
|
||||
class="box-content-row"
|
||||
appStopClick
|
||||
(click)="selectCollection(c.node)"
|
||||
>
|
||||
<div class="row-main">
|
||||
<div class="icon">
|
||||
<i class="bwi bwi-fw bwi-lg bwi-collection" aria-hidden="true"></i>
|
||||
</div>
|
||||
<span class="text">{{ c.node.name }}</span>
|
||||
</div>
|
||||
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="ciphers">
|
||||
<div *ngIf="!ciphers.length">
|
||||
<app-vault-select
|
||||
*ngIf="showVaultFilter && !showGroupings()"
|
||||
(onVaultSelectionChanged)="changeVaultSelection()"
|
||||
></app-vault-select>
|
||||
<div class="no-items" *ngIf="!nestedFolders?.length && !nestedCollections?.length">
|
||||
<i class="bwi bwi-spinner bwi-spin bwi-3x" *ngIf="!loaded" aria-hidden="true"></i>
|
||||
<ng-container *ngIf="loaded">
|
||||
<img class="no-items-image" aria-hidden="true" />
|
||||
<p>{{ "noItemsInList" | i18n }}</p>
|
||||
<button type="button" (click)="addCipher()" class="btn block primary link">
|
||||
{{ "addItem" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<cdk-virtual-scroll-viewport
|
||||
itemSize="55"
|
||||
minBufferPx="400"
|
||||
maxBufferPx="600"
|
||||
*ngIf="ciphers.length"
|
||||
#virtualScrollViewport
|
||||
><app-vault-select
|
||||
*ngIf="showVaultFilter && !showGroupings()"
|
||||
(onVaultSelectionChanged)="changeVaultSelection()"
|
||||
></app-vault-select>
|
||||
<div class="box list only-list">
|
||||
<h2 class="box-header">
|
||||
{{ groupingTitle }}
|
||||
<span class="flex-right">{{ isSearching() ? ciphers.length : ciphers.length }}</span>
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<app-cipher-row
|
||||
*cdkVirtualFor="let c of ciphers; let last = last"
|
||||
[cipher]="c"
|
||||
[last]="last"
|
||||
title="{{ 'viewItem' | i18n }}"
|
||||
(onSelected)="selectCipher($event)"
|
||||
(launchEvent)="launchCipher($event)"
|
||||
></app-cipher-row>
|
||||
</div>
|
||||
</div>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
</ng-container>
|
||||
</main>
|
@ -1,316 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Location } from "@angular/common";
|
||||
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { VaultItemsComponent as BaseVaultItemsComponent } from "@bitwarden/angular/vault/components/vault-items.component";
|
||||
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
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";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
import { BrowserComponentState } from "../../../../models/browserComponentState";
|
||||
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils";
|
||||
import { VaultBrowserStateService } from "../../../services/vault-browser-state.service";
|
||||
import { VaultFilterService } from "../../../services/vault-filter.service";
|
||||
|
||||
const ComponentId = "VaultItemsComponent";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-items",
|
||||
templateUrl: "vault-items.component.html",
|
||||
})
|
||||
export class VaultItemsComponent extends BaseVaultItemsComponent implements OnInit, OnDestroy {
|
||||
groupingTitle: string;
|
||||
state: BrowserComponentState;
|
||||
folderId: string = null;
|
||||
collectionId: string = null;
|
||||
type: CipherType = null;
|
||||
nestedFolders: TreeNode<FolderView>[];
|
||||
nestedCollections: TreeNode<CollectionView>[];
|
||||
searchTypeSearch = false;
|
||||
showOrganizations = false;
|
||||
vaultFilter: VaultFilter;
|
||||
deleted = true;
|
||||
noneFolder = false;
|
||||
showVaultFilter = false;
|
||||
|
||||
private selectedTimeout: number;
|
||||
private preventSelected = false;
|
||||
private applySavedState = true;
|
||||
private scrollingContainer = "cdk-virtual-scroll-viewport";
|
||||
|
||||
constructor(
|
||||
searchService: SearchService,
|
||||
private organizationService: OrganizationService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private location: Location,
|
||||
private ngZone: NgZone,
|
||||
private broadcasterService: BroadcasterService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private stateService: VaultBrowserStateService,
|
||||
private i18nService: I18nService,
|
||||
private collectionService: CollectionService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
cipherService: CipherService,
|
||||
private vaultFilterService: VaultFilterService,
|
||||
) {
|
||||
super(searchService, cipherService);
|
||||
this.applySavedState =
|
||||
(window as any).previousPopupUrl != null &&
|
||||
!(window as any).previousPopupUrl.startsWith("/ciphers");
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.searchTypeSearch = !this.platformUtilsService.isSafari();
|
||||
this.showOrganizations = await this.organizationService.hasOrganizations();
|
||||
this.vaultFilter = this.vaultFilterService.getVaultFilter();
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.queryParams.pipe(first()).subscribe(async (params) => {
|
||||
if (this.applySavedState) {
|
||||
this.state = await this.stateService.getBrowserVaultItemsComponentState();
|
||||
if (this.state?.searchText) {
|
||||
this.searchText = this.state.searchText;
|
||||
}
|
||||
}
|
||||
|
||||
if (params.deleted) {
|
||||
this.showVaultFilter = true;
|
||||
this.groupingTitle = this.i18nService.t("trash");
|
||||
this.searchPlaceholder = this.i18nService.t("searchTrash");
|
||||
await this.load(this.buildFilter(), true);
|
||||
} else if (params.type) {
|
||||
this.showVaultFilter = true;
|
||||
this.searchPlaceholder = this.i18nService.t("searchType");
|
||||
this.type = parseInt(params.type, null);
|
||||
switch (this.type) {
|
||||
case CipherType.Login:
|
||||
this.groupingTitle = this.i18nService.t("logins");
|
||||
break;
|
||||
case CipherType.Card:
|
||||
this.groupingTitle = this.i18nService.t("cards");
|
||||
break;
|
||||
case CipherType.Identity:
|
||||
this.groupingTitle = this.i18nService.t("identities");
|
||||
break;
|
||||
case CipherType.SecureNote:
|
||||
this.groupingTitle = this.i18nService.t("secureNotes");
|
||||
break;
|
||||
case CipherType.SshKey:
|
||||
this.groupingTitle = this.i18nService.t("sshKeys");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
await this.load(this.buildFilter());
|
||||
} else if (params.folderId) {
|
||||
this.showVaultFilter = true;
|
||||
this.folderId = params.folderId === "none" ? null : params.folderId;
|
||||
this.searchPlaceholder = this.i18nService.t("searchFolder");
|
||||
if (this.folderId != null) {
|
||||
this.showOrganizations = false;
|
||||
const folderNode = await this.vaultFilterService.getFolderNested(this.folderId);
|
||||
if (folderNode != null && folderNode.node != null) {
|
||||
this.groupingTitle = folderNode.node.name;
|
||||
this.nestedFolders =
|
||||
folderNode.children != null && folderNode.children.length > 0
|
||||
? folderNode.children
|
||||
: null;
|
||||
}
|
||||
} else {
|
||||
this.noneFolder = true;
|
||||
this.groupingTitle = this.i18nService.t("noneFolder");
|
||||
}
|
||||
await this.load(this.buildFilter());
|
||||
} else if (params.collectionId) {
|
||||
this.showVaultFilter = false;
|
||||
this.collectionId = params.collectionId;
|
||||
this.searchPlaceholder = this.i18nService.t("searchCollection");
|
||||
const collectionNode = await this.collectionService.getNested(this.collectionId);
|
||||
if (collectionNode != null && collectionNode.node != null) {
|
||||
this.groupingTitle = collectionNode.node.name;
|
||||
this.nestedCollections =
|
||||
collectionNode.children != null && collectionNode.children.length > 0
|
||||
? collectionNode.children
|
||||
: null;
|
||||
}
|
||||
await this.load(
|
||||
(c) => c.collectionIds != null && c.collectionIds.indexOf(this.collectionId) > -1,
|
||||
);
|
||||
} else {
|
||||
this.showVaultFilter = true;
|
||||
this.groupingTitle = this.i18nService.t("allItems");
|
||||
await this.load(this.buildFilter());
|
||||
}
|
||||
|
||||
if (this.applySavedState && this.state != null) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
BrowserPopupUtils.setContentScrollY(window, this.state.scrollY, {
|
||||
delay: 0,
|
||||
containerSelector: this.scrollingContainer,
|
||||
});
|
||||
}
|
||||
await this.stateService.setBrowserVaultItemsComponentState(null);
|
||||
});
|
||||
|
||||
this.broadcasterService.subscribe(ComponentId, (message: any) => {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.ngZone.run(async () => {
|
||||
switch (message.command) {
|
||||
case "syncCompleted":
|
||||
if (message.successfully) {
|
||||
window.setTimeout(() => {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.refresh();
|
||||
}, 500);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this.changeDetectorRef.detectChanges();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.saveState();
|
||||
this.broadcasterService.unsubscribe(ComponentId);
|
||||
}
|
||||
|
||||
selectCipher(cipher: CipherView) {
|
||||
this.selectedTimeout = window.setTimeout(() => {
|
||||
if (!this.preventSelected) {
|
||||
super.selectCipher(cipher);
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/view-cipher"], {
|
||||
queryParams: { cipherId: cipher.id, collectionId: this.collectionId },
|
||||
});
|
||||
}
|
||||
this.preventSelected = false;
|
||||
}, 200);
|
||||
}
|
||||
|
||||
selectFolder(folder: FolderView) {
|
||||
if (folder.id != null) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/ciphers"], { queryParams: { folderId: folder.id } });
|
||||
}
|
||||
}
|
||||
|
||||
selectCollection(collection: CollectionView) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/ciphers"], { queryParams: { collectionId: collection.id } });
|
||||
}
|
||||
|
||||
async launchCipher(cipher: CipherView) {
|
||||
if (cipher.type !== CipherType.Login || !cipher.login.canLaunch) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.selectedTimeout != null) {
|
||||
window.clearTimeout(this.selectedTimeout);
|
||||
}
|
||||
this.preventSelected = true;
|
||||
await this.cipherService.updateLastLaunchedDate(cipher.id);
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
BrowserApi.createNewTab(cipher.login.launchUri);
|
||||
if (BrowserPopupUtils.inPopup(window)) {
|
||||
BrowserApi.closePopup(window);
|
||||
}
|
||||
}
|
||||
|
||||
addCipher() {
|
||||
if (this.deleted) {
|
||||
return false;
|
||||
}
|
||||
super.addCipher();
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/add-cipher"], {
|
||||
queryParams: {
|
||||
folderId: this.folderId,
|
||||
type: this.type,
|
||||
collectionId: this.collectionId,
|
||||
selectedVault: this.vaultFilter.selectedOrganizationId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
back() {
|
||||
(window as any).routeDirection = "b";
|
||||
this.location.back();
|
||||
}
|
||||
|
||||
showGroupings() {
|
||||
return (
|
||||
!this.isSearching() &&
|
||||
((this.nestedFolders && this.nestedFolders.length) ||
|
||||
(this.nestedCollections && this.nestedCollections.length))
|
||||
);
|
||||
}
|
||||
|
||||
async changeVaultSelection() {
|
||||
this.vaultFilter = this.vaultFilterService.getVaultFilter();
|
||||
await this.load(this.buildFilter(), this.deleted);
|
||||
}
|
||||
|
||||
private buildFilter(): (cipher: CipherView) => boolean {
|
||||
return (cipher) => {
|
||||
let cipherPassesFilter = true;
|
||||
if (this.deleted && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.isDeleted;
|
||||
}
|
||||
if (this.type != null && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.type === this.type;
|
||||
}
|
||||
if (this.folderId != null && this.folderId != "none" && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.folderId === this.folderId;
|
||||
}
|
||||
if (this.noneFolder) {
|
||||
cipherPassesFilter = cipher.folderId == null;
|
||||
}
|
||||
if (this.collectionId != null && cipherPassesFilter) {
|
||||
cipherPassesFilter =
|
||||
cipher.collectionIds != null && cipher.collectionIds.indexOf(this.collectionId) > -1;
|
||||
}
|
||||
if (this.vaultFilter.selectedOrganizationId != null && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.organizationId === this.vaultFilter.selectedOrganizationId;
|
||||
}
|
||||
if (this.vaultFilter.myVaultOnly && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.organizationId === null;
|
||||
}
|
||||
return cipherPassesFilter;
|
||||
};
|
||||
}
|
||||
|
||||
private async saveState() {
|
||||
this.state = {
|
||||
scrollY: BrowserPopupUtils.getContentScrollY(window, this.scrollingContainer),
|
||||
searchText: this.searchText,
|
||||
};
|
||||
await this.stateService.setBrowserVaultItemsComponentState(this.state);
|
||||
}
|
||||
}
|
@ -1,82 +0,0 @@
|
||||
<ng-container *ngIf="loaded && organizations$ | async as organizations">
|
||||
<div class="content org-filter-content" *ngIf="loaded && shouldShow(organizations)">
|
||||
<ng-container *ngIf="selectedVault$ | async as vaultFilterDisplay">
|
||||
<button
|
||||
type="button"
|
||||
#toggleVaults
|
||||
class="org-filter"
|
||||
(click)="openOverlay()"
|
||||
aria-haspopup="menu"
|
||||
aria-controls="cdk-overlay-container"
|
||||
[attr.aria-expanded]="isOpen"
|
||||
[attr.aria-label]="vaultFilterDisplay"
|
||||
>
|
||||
<span class="org-filter-text-container">
|
||||
<span class="org-filter-text-name">{{ vaultFilterDisplay }}</span
|
||||
>
|
||||
<span
|
||||
><i
|
||||
class="bwi bwi-sm"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-angle-down': !isOpen, 'bwi-angle-up': isOpen }"
|
||||
></i></span
|
||||
></span>
|
||||
</button>
|
||||
</ng-container>
|
||||
<ng-template class="vault-select-container" #vaultSelectorTemplate>
|
||||
<div
|
||||
class="vault-select"
|
||||
[@transformPanel]="'open'"
|
||||
cdkTrapFocus
|
||||
cdkTrapFocusAutoCapture
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<button type="button" appStopClick (click)="selectAllVaults()">
|
||||
<div class="vault-select-org-text-container">
|
||||
<i class="bwi bwi-fw bwi-filter vault-select-prefix-icon" aria-hidden="true"></i>
|
||||
<span class="vault-select-org-name">{{ "allVaults" | i18n }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
*ngIf="!enforcePersonalOwnership"
|
||||
appStopClick
|
||||
(click)="selectMyVault()"
|
||||
>
|
||||
<div class="vault-select-org-text-container">
|
||||
<i class="bwi bwi-fw bwi-user vault-select-prefix-icon" aria-hidden="true"></i>
|
||||
<span class="vault-select-org-name">{{ "myVault" | i18n }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
*ngFor="let organization of organizations"
|
||||
appStopClick
|
||||
(click)="selectOrganization(organization)"
|
||||
>
|
||||
<div class="vault-select-org-text-container">
|
||||
<i
|
||||
*ngIf="organization.productTierType !== 1"
|
||||
class="bwi bwi-fw bwi-business vault-select-prefix-icon"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<i
|
||||
*ngIf="organization.productTierType === 1"
|
||||
class="bwi bwi-fw bwi-family vault-select-prefix-icon"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
|
||||
<span class="vault-select-org-name">{{ organization.name }}</span
|
||||
><i
|
||||
*ngIf="!organization.enabled"
|
||||
class="bwi bwi-fw bwi-exclamation-triangle text-danger vault-select-suffix-icon"
|
||||
attr.aria-label="{{ 'organizationIsDisabled' | i18n }}"
|
||||
appA11yTitle="{{ 'organizationIsDisabled' | i18n }}"
|
||||
></i>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</ng-container>
|
@ -1,227 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { animate, state, style, transition, trigger } from "@angular/animations";
|
||||
import { ConnectedPosition, Overlay, OverlayRef } from "@angular/cdk/overlay";
|
||||
import { TemplatePortal } from "@angular/cdk/portal";
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
TemplateRef,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from "@angular/core";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
concatMap,
|
||||
map,
|
||||
merge,
|
||||
Observable,
|
||||
Subject,
|
||||
takeUntil,
|
||||
} from "rxjs";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
import { VaultFilterService } from "../../../services/vault-filter.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-select",
|
||||
templateUrl: "vault-select.component.html",
|
||||
animations: [
|
||||
trigger("transformPanel", [
|
||||
state(
|
||||
"void",
|
||||
style({
|
||||
opacity: 0,
|
||||
}),
|
||||
),
|
||||
transition(
|
||||
"void => open",
|
||||
animate(
|
||||
"100ms linear",
|
||||
style({
|
||||
opacity: 1,
|
||||
}),
|
||||
),
|
||||
),
|
||||
transition("* => void", animate("100ms linear", style({ opacity: 0 }))),
|
||||
]),
|
||||
],
|
||||
})
|
||||
export class VaultSelectComponent implements OnInit, OnDestroy {
|
||||
@Output() onVaultSelectionChanged = new EventEmitter();
|
||||
|
||||
@ViewChild("toggleVaults", { read: ElementRef })
|
||||
buttonRef: ElementRef<HTMLButtonElement>;
|
||||
@ViewChild("vaultSelectorTemplate", { read: TemplateRef }) templateRef: TemplateRef<HTMLElement>;
|
||||
|
||||
private _selectedVault = new BehaviorSubject<string | null>(null);
|
||||
|
||||
isOpen = false;
|
||||
loaded = false;
|
||||
organizations$: Observable<Organization[]>;
|
||||
selectedVault$: Observable<string | null> = this._selectedVault.asObservable();
|
||||
|
||||
enforcePersonalOwnership = false;
|
||||
overlayPosition: ConnectedPosition[] = [
|
||||
{
|
||||
originX: "start",
|
||||
originY: "bottom",
|
||||
overlayX: "start",
|
||||
overlayY: "top",
|
||||
},
|
||||
];
|
||||
|
||||
private overlayRef: OverlayRef;
|
||||
private _destroy = new Subject<void>();
|
||||
|
||||
shouldShow(organizations: Organization[]): boolean {
|
||||
return (
|
||||
(organizations.length > 0 && !this.enforcePersonalOwnership) ||
|
||||
(organizations.length > 1 && this.enforcePersonalOwnership)
|
||||
);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private vaultFilterService: VaultFilterService,
|
||||
private i18nService: I18nService,
|
||||
private overlay: Overlay,
|
||||
private viewContainerRef: ViewContainerRef,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private organizationService: OrganizationService,
|
||||
private policyService: PolicyService,
|
||||
) {}
|
||||
|
||||
@HostListener("document:keydown.escape", ["$event"])
|
||||
handleKeyboardEvent(event: KeyboardEvent) {
|
||||
if (this.isOpen) {
|
||||
event.preventDefault();
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.organizations$ = this.organizationService.memberOrganizations$
|
||||
.pipe(takeUntil(this._destroy))
|
||||
.pipe(map((orgs) => orgs.sort(Utils.getSortFunction(this.i18nService, "name"))));
|
||||
|
||||
combineLatest([
|
||||
this.organizations$,
|
||||
this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership),
|
||||
])
|
||||
.pipe(
|
||||
concatMap(async ([organizations, enforcePersonalOwnership]) => {
|
||||
this.enforcePersonalOwnership = enforcePersonalOwnership;
|
||||
|
||||
if (this.shouldShow(organizations)) {
|
||||
if (this.enforcePersonalOwnership && !this.vaultFilterService.vaultFilter.myVaultOnly) {
|
||||
const firstOrganization = organizations[0];
|
||||
this._selectedVault.next(firstOrganization.name);
|
||||
this.vaultFilterService.setVaultFilter(firstOrganization.id);
|
||||
} else if (this.vaultFilterService.vaultFilter.myVaultOnly) {
|
||||
this._selectedVault.next(this.i18nService.t(this.vaultFilterService.myVault));
|
||||
} else if (this.vaultFilterService.vaultFilter.selectedOrganizationId != null) {
|
||||
const selectedOrganization = organizations.find(
|
||||
(o) => o.id === this.vaultFilterService.vaultFilter.selectedOrganizationId,
|
||||
);
|
||||
this._selectedVault.next(selectedOrganization.name);
|
||||
} else {
|
||||
this._selectedVault.next(this.i18nService.t(this.vaultFilterService.allVaults));
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.pipe(takeUntil(this._destroy))
|
||||
.subscribe();
|
||||
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._destroy.next();
|
||||
this._destroy.complete();
|
||||
this._selectedVault.complete();
|
||||
}
|
||||
|
||||
openOverlay() {
|
||||
const viewPortHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
|
||||
const positionStrategyBuilder = this.overlay.position();
|
||||
|
||||
const positionStrategy = positionStrategyBuilder
|
||||
.flexibleConnectedTo(this.buttonRef.nativeElement)
|
||||
.withFlexibleDimensions(true)
|
||||
.withPush(true)
|
||||
.withViewportMargin(10)
|
||||
.withGrowAfterOpen(true)
|
||||
.withPositions(this.overlayPosition);
|
||||
|
||||
this.overlayRef = this.overlay.create({
|
||||
hasBackdrop: true,
|
||||
positionStrategy,
|
||||
maxHeight: viewPortHeight - 160,
|
||||
backdropClass: "cdk-overlay-transparent-backdrop",
|
||||
scrollStrategy: this.overlay.scrollStrategies.close(),
|
||||
});
|
||||
|
||||
const templatePortal = new TemplatePortal(this.templateRef, this.viewContainerRef);
|
||||
this.overlayRef.attach(templatePortal);
|
||||
this.isOpen = true;
|
||||
|
||||
// Handle closing
|
||||
merge(
|
||||
this.overlayRef.outsidePointerEvents(),
|
||||
this.overlayRef.backdropClick(),
|
||||
this.overlayRef.detachments(),
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
).subscribe(() => {
|
||||
this.close();
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.overlayRef) {
|
||||
this.overlayRef.dispose();
|
||||
this.overlayRef = undefined;
|
||||
}
|
||||
this.isOpen = false;
|
||||
}
|
||||
|
||||
selectOrganization(organization: Organization) {
|
||||
if (!organization.enabled) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
null,
|
||||
this.i18nService.t("disabledOrganizationFilterError"),
|
||||
);
|
||||
} else {
|
||||
this._selectedVault.next(organization.name);
|
||||
this.vaultFilterService.setVaultFilter(organization.id);
|
||||
this.onVaultSelectionChanged.emit();
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
selectAllVaults() {
|
||||
this._selectedVault.next(this.i18nService.t(this.vaultFilterService.allVaults));
|
||||
this.vaultFilterService.setVaultFilter(this.vaultFilterService.allVaults);
|
||||
this.onVaultSelectionChanged.emit();
|
||||
this.close();
|
||||
}
|
||||
selectMyVault() {
|
||||
this._selectedVault.next(this.i18nService.t(this.vaultFilterService.myVault));
|
||||
this.vaultFilterService.setVaultFilter(this.vaultFilterService.myVault);
|
||||
this.onVaultSelectionChanged.emit();
|
||||
this.close();
|
||||
}
|
||||
}
|
@ -1,98 +0,0 @@
|
||||
<ng-container>
|
||||
<h2 class="box-header">
|
||||
{{ "customFields" | i18n }}
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<div class="box-content-row box-content-row-flex" *ngFor="let field of cipher.fields">
|
||||
<div class="row-main">
|
||||
<span
|
||||
*ngIf="field.type != fieldType.Linked"
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, field.value)"
|
||||
>{{ field.name }}</span
|
||||
>
|
||||
<span *ngIf="field.type === fieldType.Linked" class="row-label">{{ field.name }}</span>
|
||||
<div *ngIf="field.type === fieldType.Text">
|
||||
{{ field.value || " " }}
|
||||
</div>
|
||||
<div *ngIf="field.type === fieldType.Hidden">
|
||||
<span *ngIf="!field.showValue" class="monospaced">{{ field.maskedValue }}</span>
|
||||
<span
|
||||
*ngIf="field.showValue && !field.showCount"
|
||||
class="monospaced show-whitespace"
|
||||
[innerHTML]="field.value | colorPassword"
|
||||
></span>
|
||||
<span
|
||||
*ngIf="field.showValue && field.showCount"
|
||||
[innerHTML]="field.value | colorPasswordCount"
|
||||
></span>
|
||||
</div>
|
||||
<div *ngIf="field.type === fieldType.Boolean">
|
||||
<i class="bwi bwi-check-square" *ngIf="field.value === 'true'" aria-hidden="true"></i>
|
||||
<i class="bwi bwi-square" *ngIf="field.value !== 'true'" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{ field.value }}</span>
|
||||
</div>
|
||||
<div *ngIf="field.type === fieldType.Linked" class="box-content-row-flex">
|
||||
<div class="icon icon-small">
|
||||
<i
|
||||
class="bwi bwi-link"
|
||||
aria-hidden="true"
|
||||
appA11yTitle="{{ 'linkedValue' | i18n }}"
|
||||
></i>
|
||||
<span class="sr-only">{{ "linkedValue" | i18n }}</span>
|
||||
</div>
|
||||
<span>{{ cipher.linkedFieldI18nKey(field.linkedId) | i18n }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-buttons action-buttons-fixed">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
attr.aria-label="{{ 'toggleCharacterCount' | i18n }} {{ field.name }}"
|
||||
appA11yTitle="{{ 'toggleCharacterCount' | i18n }}"
|
||||
*ngIf="field.type === fieldType.Hidden && cipher.viewPassword && field.showValue"
|
||||
(click)="toggleFieldCount(field)"
|
||||
[attr.aria-pressed]="field.showCount"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-numbered-list" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
attr.aria-label="{{ 'toggleVisibility' | i18n }} {{ field.name }}"
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
*ngIf="field.type === fieldType.Hidden && cipher.viewPassword"
|
||||
(click)="toggleFieldValue(field)"
|
||||
[attr.aria-pressed]="field.showValue"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !field.showValue, 'bwi-eye-slash': field.showValue }"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
attr.aria-label="{{ 'copyValue' | i18n }} {{ field.name }}"
|
||||
appA11yTitle="{{ 'copyValue' | i18n }}"
|
||||
*ngIf="
|
||||
field.value &&
|
||||
field.type !== fieldType.Boolean &&
|
||||
field.type !== fieldType.Linked &&
|
||||
!(field.type === fieldType.Hidden && !cipher.viewPassword)
|
||||
"
|
||||
(click)="
|
||||
copy(field.value, 'value', field.type === fieldType.Hidden ? 'H_Field' : 'Field')
|
||||
"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
@ -1,14 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { ViewCustomFieldsComponent as BaseViewCustomFieldsComponent } from "@bitwarden/angular/vault/components/view-custom-fields.component";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-view-custom-fields",
|
||||
templateUrl: "view-custom-fields.component.html",
|
||||
})
|
||||
export class ViewCustomFieldsComponent extends BaseViewCustomFieldsComponent {
|
||||
constructor(eventCollectionService: EventCollectionService) {
|
||||
super(eventCollectionService);
|
||||
}
|
||||
}
|
@ -1,719 +0,0 @@
|
||||
<header>
|
||||
<div class="left">
|
||||
<button type="button" (click)="close()">{{ "close" | i18n }}</button>
|
||||
</div>
|
||||
<h1 class="center">
|
||||
<span class="title">{{ "viewItem" | i18n }}</span>
|
||||
</h1>
|
||||
<div class="right" *ngIf="cipher">
|
||||
<button type="button" (click)="edit()" *ngIf="!cipher.isDeleted">
|
||||
{{ "edit" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<main tabindex="-1" *ngIf="cipher">
|
||||
<div class="box">
|
||||
<h2 class="box-header">
|
||||
{{ "itemInformation" | i18n }}
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<div class="box-content-row">
|
||||
<label
|
||||
for="name"
|
||||
class="draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.name)"
|
||||
>{{ "name" | i18n }}</label
|
||||
>
|
||||
<input id="name" type="text" [value]="cipher.name" readonly aria-readonly="true" />
|
||||
</div>
|
||||
<!-- Login -->
|
||||
<div *ngIf="cipher.login">
|
||||
<div class="box-content-row box-content-row-flex" *ngIf="cipher.login.username">
|
||||
<div class="row-main">
|
||||
<label
|
||||
for="loginUsername"
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.login.username)"
|
||||
>{{ "username" | i18n }}
|
||||
</label>
|
||||
<input
|
||||
id="loginUsername"
|
||||
type="text"
|
||||
[value]="cipher.login.username"
|
||||
readonly
|
||||
aria-readonly="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'copyUsername' | i18n }}"
|
||||
(click)="copy(cipher.login.username, 'username', 'Username')"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-flex" *ngIf="cipher.login.password">
|
||||
<div class="row-main">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.login.password)"
|
||||
>{{ "password" | i18n }}</span
|
||||
>
|
||||
<div *ngIf="!showPassword" class="monospaced">
|
||||
{{ cipher.login.maskedPassword }}
|
||||
</div>
|
||||
<div
|
||||
*ngIf="showPassword && !showPasswordCount"
|
||||
class="monospaced password-wrapper"
|
||||
[appCopyText]="cipher.login.password"
|
||||
[innerHTML]="cipher.login.password | colorPassword"
|
||||
></div>
|
||||
<div
|
||||
*ngIf="showPassword && showPasswordCount"
|
||||
[innerHTML]="cipher.login.password | colorPasswordCount"
|
||||
></div>
|
||||
</div>
|
||||
<div class="action-buttons action-buttons-fixed">
|
||||
<button
|
||||
type="button"
|
||||
#checkPasswordBtn
|
||||
class="row-btn btn"
|
||||
appA11yTitle="{{ 'checkPassword' | i18n }}"
|
||||
(click)="checkPassword()"
|
||||
[appApiAction]="checkPasswordPromise"
|
||||
[disabled]="$any(checkPasswordBtn).loading"
|
||||
*ngIf="cipher.viewPassword"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg bwi-check-circle"
|
||||
[hidden]="$any(checkPasswordBtn).loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<i
|
||||
class="bwi bwi-lg bwi-spinner bwi-spin"
|
||||
[hidden]="!$any(checkPasswordBtn).loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
attr.aria-label="{{ 'toggleCharacterCount' | i18n }} {{ 'password' | i18n }}"
|
||||
appA11yTitle="{{ 'toggleCharacterCount' | i18n }}"
|
||||
(click)="togglePasswordCount()"
|
||||
*ngIf="showPassword"
|
||||
[attr.aria-pressed]="showPasswordCount"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-numbered-list" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
attr.aria-label="{{ 'toggleVisibility' | i18n }} {{ 'password' | i18n }}"
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
(click)="togglePassword()"
|
||||
*ngIf="cipher.viewPassword"
|
||||
[attr.aria-pressed]="showPassword"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'copyPassword' | i18n }}"
|
||||
(click)="copy(cipher.login.password, 'password', 'Password')"
|
||||
*ngIf="cipher.viewPassword"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--Passkey-->
|
||||
<div
|
||||
class="box"
|
||||
*ngIf="cipher.login.hasFido2Credentials"
|
||||
tabindex="0"
|
||||
attr.aria-label="{{ 'typePasskey' | i18n }} {{ fido2CredentialCreationDateValue }}"
|
||||
>
|
||||
<div class="box-content">
|
||||
<div class="box-content-row text-muted">
|
||||
<span class="row-label">{{ "typePasskey" | i18n }}</span>
|
||||
{{ fido2CredentialCreationDateValue }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="box-content-row box-content-row-flex totp"
|
||||
[ngClass]="{ low: totpLow }"
|
||||
*ngIf="cipher.login.totp && totpCode"
|
||||
>
|
||||
<div class="row-main">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, totpCode)"
|
||||
>{{ "verificationCodeTotp" | i18n }}</span
|
||||
>
|
||||
<span class="totp-code">{{ totpCodeFormatted }}</span>
|
||||
</div>
|
||||
<span class="totp-countdown" aria-hidden="true">
|
||||
<span class="totp-sec">{{ totpSec }}</span>
|
||||
<svg>
|
||||
<g>
|
||||
<circle
|
||||
class="totp-circle inner"
|
||||
r="12.6"
|
||||
cy="16"
|
||||
cx="16"
|
||||
[ngStyle]="{ 'stroke-dashoffset.px': totpDash }"
|
||||
></circle>
|
||||
<circle class="totp-circle outer" r="14" cy="16" cx="16"></circle>
|
||||
</g>
|
||||
</svg>
|
||||
</span>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
title="{{ 'copyVerificationCode' | i18n }}"
|
||||
(click)="copy(totpCode, 'verificationCodeTotp', 'TOTP')"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{ "copyValue" | i18n }}</span>
|
||||
<span
|
||||
class="sr-only exists-only-on-parent-focus"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
>{{ totpSec }}</span
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-flex totp" *ngIf="showPremiumRequiredTotp">
|
||||
<div class="row-main">
|
||||
<span class="row-label">{{ "verificationCodeTotp" | i18n }}</span>
|
||||
<span class="row-label">
|
||||
<a routerLink="/premium">
|
||||
{{ "premiumSubcriptionRequired" | i18n }}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Card -->
|
||||
<div *ngIf="cipher.card">
|
||||
<div class="box-content-row" *ngIf="cipher.card.cardholderName">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.card.cardholderName)"
|
||||
>{{ "cardholderName" | i18n }}</span
|
||||
>
|
||||
{{ cipher.card.cardholderName }}
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-flex" *ngIf="cipher.card.number">
|
||||
<div class="row-main">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.card.number)"
|
||||
>{{ "number" | i18n }}</span
|
||||
>
|
||||
<span [hidden]="showCardNumber" class="monospaced">{{
|
||||
cipher.card.maskedNumber | creditCardNumber: cipher.card.brand
|
||||
}}</span>
|
||||
<span [hidden]="!showCardNumber" class="monospaced">{{
|
||||
cipher.card.number | creditCardNumber: cipher.card.brand
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
attr.aria-label="{{ 'toggleVisibility' | i18n }} {{ 'number' | i18n }}"
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
(click)="toggleCardNumber()"
|
||||
[attr.aria-pressed]="showCardNumber"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !showCardNumber, 'bwi-eye-slash': showCardNumber }"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'copyNumber' | i18n }}"
|
||||
(click)="copy(cipher.card.number, 'number', 'Card Number')"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.card.brand">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.card.brand)"
|
||||
>{{ "brand" | i18n }}</span
|
||||
>
|
||||
{{ cipher.card.brand }}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.card.expiration">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.card.expiration)"
|
||||
>{{ "expiration" | i18n }}</span
|
||||
>
|
||||
{{ cipher.card.expiration }}
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-flex" *ngIf="cipher.card.code">
|
||||
<div class="row-main">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.card.code)"
|
||||
>{{ "securityCode" | i18n }}</span
|
||||
>
|
||||
<span [hidden]="showCardCode" class="monospaced">{{ cipher.card.maskedCode }}</span>
|
||||
<span [hidden]="!showCardCode" class="monospaced">{{ cipher.card.code }}</span>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
attr.aria-label="{{ 'toggleVisibility' | i18n }} {{ 'securityCode' | i18n }}"
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
(click)="toggleCardCode()"
|
||||
[attr.aria-pressed]="showCardCode"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !showCardCode, 'bwi-eye-slash': showCardCode }"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'copySecurityCode' | i18n }}"
|
||||
(click)="copy(cipher.card.code, 'securityCode', 'Security Code')"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Identity -->
|
||||
<div *ngIf="cipher.identity">
|
||||
<div class="box-content-row" *ngIf="cipher.identity.fullName">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.identity.fullName)"
|
||||
>{{ "identityName" | i18n }}</span
|
||||
>
|
||||
{{ cipher.identity.fullName }}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.identity.username">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.identity.username)"
|
||||
>{{ "username" | i18n }}</span
|
||||
>
|
||||
{{ cipher.identity.username }}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.identity.company">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.identity.company)"
|
||||
>{{ "company" | i18n }}</span
|
||||
>
|
||||
{{ cipher.identity.company }}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.identity.ssn">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.identity.ssn)"
|
||||
>{{ "ssn" | i18n }}</span
|
||||
>
|
||||
{{ cipher.identity.ssn }}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.identity.passportNumber">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.identity.passportNumber)"
|
||||
>{{ "passportNumber" | i18n }}</span
|
||||
>
|
||||
{{ cipher.identity.passportNumber }}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.identity.licenseNumber">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.identity.licenseNumber)"
|
||||
>{{ "licenseNumber" | i18n }}</span
|
||||
>
|
||||
{{ cipher.identity.licenseNumber }}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.identity.email">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.identity.email)"
|
||||
>{{ "email" | i18n }}</span
|
||||
>
|
||||
{{ cipher.identity.email }}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.identity.phone">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.identity.phone)"
|
||||
>{{ "phone" | i18n }}</span
|
||||
>
|
||||
{{ cipher.identity.phone }}
|
||||
</div>
|
||||
<div
|
||||
class="box-content-row"
|
||||
*ngIf="cipher.identity.address1 || cipher.identity.city || cipher.identity.country"
|
||||
>
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="
|
||||
setTextDataOnDrag(
|
||||
$event,
|
||||
(cipher.identity.address1 ? cipher.identity.address1 + '\n' : '') +
|
||||
(cipher.identity.address2 ? cipher.identity.address2 + '\n' : '') +
|
||||
(cipher.identity.address3 ? cipher.identity.address3 + '\n' : '') +
|
||||
(cipher.identity.fullAddressPart2
|
||||
? cipher.identity.fullAddressPart2 + '\n'
|
||||
: '') +
|
||||
(cipher.identity.country ? cipher.identity.country : '')
|
||||
)
|
||||
"
|
||||
>{{ "address" | i18n }}</span
|
||||
>
|
||||
<div *ngIf="cipher.identity.address1">{{ cipher.identity.address1 }}</div>
|
||||
<div *ngIf="cipher.identity.address2">{{ cipher.identity.address2 }}</div>
|
||||
<div *ngIf="cipher.identity.address3">{{ cipher.identity.address3 }}</div>
|
||||
<div *ngIf="cipher.identity.fullAddressPart2">{{ cipher.identity.fullAddressPart2 }}</div>
|
||||
<div *ngIf="cipher.identity.country">{{ cipher.identity.country }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- SshKey -->
|
||||
<div *ngIf="cipher.sshKey">
|
||||
<div class="box-content-row" *ngIf="cipher.sshKey.privateKey" style="overflow: hidden">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.sshKey.privateKey)"
|
||||
>
|
||||
{{ "sshPrivateKey" | i18n }}
|
||||
</span>
|
||||
<div [innerText]="cipher.sshKey.maskedPrivateKey" class="monospaced"></div>
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.sshKey.publicKey" style="overflow: hidden">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.sshKey.publicKey)"
|
||||
>
|
||||
{{ "sshPublicKey" | i18n }}</span
|
||||
>
|
||||
{{ cipher.sshKey.publicKey }}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.sshKey.keyFingerprint" style="overflow: hidden">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.sshKey.keyFingerprint)"
|
||||
>
|
||||
{{ "sshFingerprint" | i18n }}</span
|
||||
>
|
||||
{{ cipher.sshKey.keyFingerprint }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" *ngIf="cipher.login && cipher.login.hasUris">
|
||||
<div class="box-content">
|
||||
<div
|
||||
class="box-content-row box-content-row-flex"
|
||||
*ngFor="let u of cipher.login.uris; let i = index"
|
||||
>
|
||||
<div class="row-main">
|
||||
<label
|
||||
for="hostOrUri{{ i }}"
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, u.uri)"
|
||||
*ngIf="!u.isWebsite"
|
||||
>{{ "uri" | i18n }}</label
|
||||
>
|
||||
<label
|
||||
for="hostOrUri{{ i }}"
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, u.uri)"
|
||||
*ngIf="u.isWebsite"
|
||||
>{{ "website" | i18n }}</label
|
||||
>
|
||||
<span title="{{ u.uri }}">
|
||||
<input
|
||||
id="hostOrUri{{ i }}"
|
||||
type="text"
|
||||
[value]="u.hostOrUri"
|
||||
readonly
|
||||
aria-readonly="true"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
attr.aria-label="{{ 'launch' | i18n }} {{ u.uri }}"
|
||||
appA11yTitle="{{ 'launch' | i18n }}"
|
||||
*ngIf="u.canLaunch"
|
||||
(click)="launch(u, cipher.id)"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-share-square" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
attr.aria-label="{{ 'copyUri' | i18n }} {{ u.uri }}"
|
||||
appA11yTitle="{{ 'copyUri' | i18n }}"
|
||||
(click)="copy(u.uri, u.isWebsite ? 'website' : 'uri', 'URI')"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" *ngIf="cipher.folderId && folder">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row">
|
||||
<label
|
||||
for="folderName"
|
||||
class="draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, folder.name)"
|
||||
>{{ "folder" | i18n }}</label
|
||||
>
|
||||
<input id="folderName" type="text" name="folderName" [value]="folder.name" readonly />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" *ngIf="cipher.notes">
|
||||
<h2 class="box-header">
|
||||
<label
|
||||
for="notes"
|
||||
class="draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.notes)"
|
||||
>{{ "notes" | i18n }}</label
|
||||
>
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<div class="box-content-row">
|
||||
<textarea
|
||||
id="notes"
|
||||
[value]="cipher.notes"
|
||||
rows="6"
|
||||
readonly
|
||||
aria-readonly="true"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" *ngIf="cipher.hasFields">
|
||||
<app-vault-view-custom-fields
|
||||
[cipher]="cipher"
|
||||
[promptPassword]="promptPassword.bind(this)"
|
||||
[copy]="copy.bind(this)"
|
||||
></app-vault-view-custom-fields>
|
||||
</div>
|
||||
<div
|
||||
class="box"
|
||||
*ngIf="cipher.hasAttachments && (canAccessPremium || cipher.organizationId) && showAttachments"
|
||||
>
|
||||
<h2 class="box-header">
|
||||
{{ "attachments" | i18n }}
|
||||
</h2>
|
||||
<div class="box-content single-line">
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row box-content-row-flex text-default"
|
||||
*ngFor="let attachment of cipher.attachments"
|
||||
appStopClick
|
||||
(click)="downloadAttachment(attachment)"
|
||||
>
|
||||
<span class="row-main">{{ attachment.fileName }}</span>
|
||||
<small class="row-sub-label">{{ attachment.sizeName }}</small>
|
||||
<i
|
||||
class="bwi bwi-download bwi-fw row-sub-icon"
|
||||
*ngIf="!$any(attachment).downloading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-fw bwi-spin row-sub-icon"
|
||||
*ngIf="$any(attachment).downloading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box list">
|
||||
<div class="box-content single-line">
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row"
|
||||
appStopClick
|
||||
(click)="fillCipher()"
|
||||
*ngIf="
|
||||
cipher.type !== cipherType.SecureNote &&
|
||||
!cipher.isDeleted &&
|
||||
(!this.inPopout || this.loadAction)
|
||||
"
|
||||
>
|
||||
<div class="row-main text-primary">
|
||||
<div class="icon text-primary" aria-hidden="true">
|
||||
<i class="bwi bwi-pencil-square bwi-lg bwi-fw"></i>
|
||||
</div>
|
||||
<span>{{ "autoFill" | i18n }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row"
|
||||
appStopClick
|
||||
(click)="fillCipherAndSave()"
|
||||
*ngIf="cipher.type === cipherType.Login && !cipher.isDeleted && !inPopout"
|
||||
>
|
||||
<div class="row-main text-primary">
|
||||
<div class="icon text-primary" aria-hidden="true">
|
||||
<i class="bwi bwi-bookmark bwi-lg bwi-fw"></i>
|
||||
</div>
|
||||
<span>{{ "autoFillAndSave" | i18n }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row"
|
||||
appStopClick
|
||||
(click)="clone()"
|
||||
*ngIf="!cipher.organizationId && !cipher.isDeleted"
|
||||
>
|
||||
<div class="row-main text-primary">
|
||||
<div class="icon text-primary" aria-hidden="true">
|
||||
<i class="bwi bwi-files bwi-lg bwi-fw"></i>
|
||||
</div>
|
||||
<span>{{ "cloneItem" | i18n }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row"
|
||||
appStopClick
|
||||
(click)="share()"
|
||||
*ngIf="!cipher.organizationId"
|
||||
>
|
||||
<div class="row-main text-primary">
|
||||
<div class="icon text-primary" aria-hidden="true">
|
||||
<i class="bwi bwi-arrow-circle-right bwi-lg bwi-fw"></i>
|
||||
</div>
|
||||
<span>{{ "moveToOrganization" | i18n }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row"
|
||||
appStopClick
|
||||
(click)="restore()"
|
||||
*ngIf="cipher.isDeleted"
|
||||
>
|
||||
<div class="row-main text-primary">
|
||||
<div class="icon text-primary" aria-hidden="true">
|
||||
<i class="bwi bwi-undo bwi-lg bwi-fw"></i>
|
||||
</div>
|
||||
<span>{{ "restoreItem" | i18n }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row"
|
||||
appStopClick
|
||||
(click)="delete()"
|
||||
*ngIf="canDeleteCipher$ | async"
|
||||
>
|
||||
<div class="row-main text-danger">
|
||||
<div class="icon text-danger" aria-hidden="true">
|
||||
<i class="bwi bwi-trash bwi-lg bwi-fw"></i>
|
||||
</div>
|
||||
<span>{{ (cipher.isDeleted ? "permanentlyDeleteItem" : "deleteItem") | i18n }}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="box-footer">
|
||||
<div>
|
||||
<b class="font-weight-semibold">{{ "dateUpdated" | i18n }}:</b>
|
||||
{{ cipher.revisionDate | date: "medium" }}
|
||||
</div>
|
||||
<div *ngIf="cipher.creationDate">
|
||||
<b class="font-weight-semibold">{{ "dateCreated" | i18n }}:</b>
|
||||
{{ cipher.creationDate | date: "medium" }}
|
||||
</div>
|
||||
<div *ngIf="cipher.passwordRevisionDisplayDate">
|
||||
<b class="font-weight-semibold">{{ "datePasswordUpdated" | i18n }}:</b>
|
||||
{{ cipher.passwordRevisionDisplayDate | date: "medium" }}
|
||||
</div>
|
||||
<div *ngIf="cipher.hasPasswordHistory">
|
||||
<b class="font-weight-semibold">{{ "passwordHistory" | i18n }}:</b>
|
||||
<button
|
||||
type="button"
|
||||
routerLink="/cipher-password-history"
|
||||
[queryParams]="{ cipherId: cipher.id }"
|
||||
appStopClick
|
||||
title="{{ 'passwordHistory' | i18n }}"
|
||||
>
|
||||
{{ cipher.passwordHistory.length }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
@ -1,443 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DatePipe, Location } from "@angular/common";
|
||||
import { ChangeDetectorRef, Component, NgZone, OnInit, OnDestroy } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { Subject, firstValueFrom, takeUntil, Subscription } from "rxjs";
|
||||
import { first, map } from "rxjs/operators";
|
||||
|
||||
import { ViewComponent as BaseViewComponent } from "@bitwarden/angular/vault/components/view.component";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { BrowserFido2UserInterfaceSession } from "../../../../autofill/fido2/services/browser-fido2-user-interface.service";
|
||||
import { AutofillService } from "../../../../autofill/services/abstractions/autofill.service";
|
||||
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils";
|
||||
import { fido2PopoutSessionData$ } from "../../utils/fido2-popout-session-data";
|
||||
import { closeViewVaultItemPopout, VaultPopoutType } from "../../utils/vault-popout-window";
|
||||
|
||||
const BroadcasterSubscriptionId = "ChildViewComponent";
|
||||
|
||||
export const AUTOFILL_ID = "autofill";
|
||||
export const SHOW_AUTOFILL_BUTTON = "show-autofill-button";
|
||||
export const COPY_USERNAME_ID = "copy-username";
|
||||
export const COPY_PASSWORD_ID = "copy-password";
|
||||
export const COPY_VERIFICATION_CODE_ID = "copy-totp";
|
||||
|
||||
type CopyAction =
|
||||
| typeof COPY_USERNAME_ID
|
||||
| typeof COPY_PASSWORD_ID
|
||||
| typeof COPY_VERIFICATION_CODE_ID;
|
||||
type LoadAction = typeof AUTOFILL_ID | typeof SHOW_AUTOFILL_BUTTON | CopyAction;
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-view",
|
||||
templateUrl: "view.component.html",
|
||||
})
|
||||
export class ViewComponent extends BaseViewComponent implements OnInit, OnDestroy {
|
||||
showAttachments = true;
|
||||
pageDetails: any[] = [];
|
||||
tab: any;
|
||||
senderTabId?: number;
|
||||
loadAction?: LoadAction;
|
||||
private static readonly copyActions = new Set([
|
||||
COPY_USERNAME_ID,
|
||||
COPY_PASSWORD_ID,
|
||||
COPY_VERIFICATION_CODE_ID,
|
||||
]);
|
||||
uilocation?: "popout" | "popup" | "sidebar" | "tab";
|
||||
loadPageDetailsTimeout: number;
|
||||
inPopout = false;
|
||||
cipherType = CipherType;
|
||||
private fido2PopoutSessionData$ = fido2PopoutSessionData$();
|
||||
private collectPageDetailsSubscription: Subscription;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
cipherService: CipherService,
|
||||
folderService: FolderService,
|
||||
totpService: TotpServiceAbstraction,
|
||||
tokenService: TokenService,
|
||||
i18nService: I18nService,
|
||||
keyService: KeyService,
|
||||
encryptService: EncryptService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
auditService: AuditService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private location: Location,
|
||||
broadcasterService: BroadcasterService,
|
||||
ngZone: NgZone,
|
||||
changeDetectorRef: ChangeDetectorRef,
|
||||
stateService: StateService,
|
||||
eventCollectionService: EventCollectionService,
|
||||
private autofillService: AutofillService,
|
||||
private messagingService: MessagingService,
|
||||
apiService: ApiService,
|
||||
passwordRepromptService: PasswordRepromptService,
|
||||
logService: LogService,
|
||||
fileDownloadService: FileDownloadService,
|
||||
dialogService: DialogService,
|
||||
datePipe: DatePipe,
|
||||
accountService: AccountService,
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
cipherAuthorizationService: CipherAuthorizationService,
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
folderService,
|
||||
totpService,
|
||||
tokenService,
|
||||
i18nService,
|
||||
keyService,
|
||||
encryptService,
|
||||
platformUtilsService,
|
||||
auditService,
|
||||
window,
|
||||
broadcasterService,
|
||||
ngZone,
|
||||
changeDetectorRef,
|
||||
eventCollectionService,
|
||||
apiService,
|
||||
passwordRepromptService,
|
||||
logService,
|
||||
stateService,
|
||||
fileDownloadService,
|
||||
dialogService,
|
||||
datePipe,
|
||||
accountService,
|
||||
billingAccountProfileStateService,
|
||||
cipherAuthorizationService,
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((value) => {
|
||||
this.loadAction = value?.action;
|
||||
this.senderTabId = parseInt(value?.senderTabId, 10) || undefined;
|
||||
this.uilocation = value?.uilocation;
|
||||
});
|
||||
|
||||
this.inPopout = this.uilocation === "popout" || BrowserPopupUtils.inPopout(window);
|
||||
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.queryParams.pipe(first()).subscribe(async (params) => {
|
||||
if (params.cipherId) {
|
||||
this.cipherId = params.cipherId;
|
||||
}
|
||||
|
||||
if (params.collectionId) {
|
||||
this.collectionId = params.collectionId;
|
||||
}
|
||||
|
||||
if (!params.cipherId) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.close();
|
||||
}
|
||||
|
||||
await this.load();
|
||||
});
|
||||
|
||||
super.ngOnInit();
|
||||
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.ngZone.run(async () => {
|
||||
switch (message.command) {
|
||||
case "tabChanged":
|
||||
case "windowChanged":
|
||||
if (this.loadPageDetailsTimeout != null) {
|
||||
window.clearTimeout(this.loadPageDetailsTimeout);
|
||||
}
|
||||
this.loadPageDetailsTimeout = window.setTimeout(() => this.loadPageDetails(), 500);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
super.ngOnDestroy();
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
}
|
||||
|
||||
async load() {
|
||||
await super.load();
|
||||
await this.loadPageDetails();
|
||||
await this.handleLoadAction();
|
||||
}
|
||||
|
||||
async edit() {
|
||||
if (this.cipher.isDeleted) {
|
||||
return false;
|
||||
}
|
||||
if (!(await super.edit())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/edit-cipher"], {
|
||||
queryParams: {
|
||||
cipherId: this.cipher.id,
|
||||
type: this.cipher.type,
|
||||
isNew: false,
|
||||
collectionId: this.collectionId,
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
async clone() {
|
||||
if (this.cipher.isDeleted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(await super.clone())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/clone-cipher"], {
|
||||
queryParams: {
|
||||
cloneMode: true,
|
||||
cipherId: this.cipher.id,
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
async share() {
|
||||
if (!(await super.share())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.cipher.organizationId == null) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/share-cipher"], {
|
||||
replaceUrl: true,
|
||||
queryParams: { cipherId: this.cipher.id },
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async fillCipher() {
|
||||
const didAutofill = await this.doAutofill();
|
||||
if (didAutofill) {
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("autoFillSuccess"));
|
||||
}
|
||||
|
||||
return didAutofill;
|
||||
}
|
||||
|
||||
async fillCipherAndSave() {
|
||||
const didAutofill = await this.doAutofill();
|
||||
|
||||
if (didAutofill) {
|
||||
if (this.tab == null) {
|
||||
throw new Error("No tab found.");
|
||||
}
|
||||
|
||||
if (this.cipher.login.uris == null) {
|
||||
this.cipher.login.uris = [];
|
||||
} else {
|
||||
if (this.cipher.login.uris.some((uri) => uri.uri === this.tab.url)) {
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("autoFillSuccessAndSavedUri"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const loginUri = new LoginUriView();
|
||||
loginUri.uri = this.tab.url;
|
||||
this.cipher.login.uris.push(loginUri);
|
||||
|
||||
try {
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const cipher: Cipher = await this.cipherService.encrypt(this.cipher, activeUserId);
|
||||
await this.cipherService.updateWithServer(cipher);
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("autoFillSuccessAndSavedUri"),
|
||||
);
|
||||
this.messagingService.send("editedCipher");
|
||||
} catch {
|
||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("unexpectedError"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async restore() {
|
||||
if (!this.cipher.isDeleted) {
|
||||
return false;
|
||||
}
|
||||
if (await super.restore()) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.close();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async delete() {
|
||||
if (await super.delete()) {
|
||||
this.messagingService.send("deletedCipher");
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.close();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async close() {
|
||||
const sessionData = await firstValueFrom(this.fido2PopoutSessionData$);
|
||||
if (this.inPopout && sessionData.isFido2Session) {
|
||||
BrowserFido2UserInterfaceSession.abortPopout(sessionData.sessionId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
BrowserPopupUtils.inSingleActionPopout(window, VaultPopoutType.viewVaultItem) &&
|
||||
this.senderTabId
|
||||
) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
BrowserApi.focusTab(this.senderTabId);
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
closeViewVaultItemPopout(`${VaultPopoutType.viewVaultItem}_${this.cipher.id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.location.back();
|
||||
}
|
||||
|
||||
private async loadPageDetails() {
|
||||
this.collectPageDetailsSubscription?.unsubscribe();
|
||||
this.pageDetails = [];
|
||||
this.tab = this.senderTabId
|
||||
? await BrowserApi.getTab(this.senderTabId)
|
||||
: await BrowserApi.getTabFromCurrentWindow();
|
||||
|
||||
if (!this.tab) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.collectPageDetailsSubscription = this.autofillService
|
||||
.collectPageDetailsFromTab$(this.tab)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((pageDetails) => (this.pageDetails = pageDetails));
|
||||
}
|
||||
|
||||
private async doAutofill() {
|
||||
const originalTabURL = this.tab.url?.length && new URL(this.tab.url);
|
||||
|
||||
if (!(await this.promptPassword())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentTabURL = this.tab.url?.length && new URL(this.tab.url);
|
||||
|
||||
const originalTabHostPath =
|
||||
originalTabURL && `${originalTabURL.origin}${originalTabURL.pathname}`;
|
||||
const currentTabHostPath = currentTabURL && `${currentTabURL.origin}${currentTabURL.pathname}`;
|
||||
|
||||
const tabUrlChanged = originalTabHostPath !== currentTabHostPath;
|
||||
|
||||
if (this.pageDetails == null || this.pageDetails.length === 0 || tabUrlChanged) {
|
||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("autofillError"));
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
this.totpCode = await this.autofillService.doAutoFill({
|
||||
tab: this.tab,
|
||||
cipher: this.cipher,
|
||||
pageDetails: this.pageDetails,
|
||||
doc: window.document,
|
||||
fillNewPassword: true,
|
||||
allowTotpAutofill: true,
|
||||
});
|
||||
if (this.totpCode != null) {
|
||||
this.platformUtilsService.copyToClipboard(this.totpCode, { window: window });
|
||||
}
|
||||
} catch {
|
||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("autofillError"));
|
||||
this.changeDetectorRef.detectChanges();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async handleLoadAction() {
|
||||
if (!this.loadAction || this.loadAction === SHOW_AUTOFILL_BUTTON) {
|
||||
return;
|
||||
}
|
||||
|
||||
let loadActionSuccess = false;
|
||||
if (this.loadAction === AUTOFILL_ID) {
|
||||
loadActionSuccess = await this.fillCipher();
|
||||
}
|
||||
|
||||
if (ViewComponent.copyActions.has(this.loadAction)) {
|
||||
const { username, password } = this.cipher.login;
|
||||
const copyParams: Record<CopyAction, Record<string, string>> = {
|
||||
[COPY_USERNAME_ID]: { value: username, type: "username", name: "Username" },
|
||||
[COPY_PASSWORD_ID]: { value: password, type: "password", name: "Password" },
|
||||
[COPY_VERIFICATION_CODE_ID]: {
|
||||
value: this.totpCode,
|
||||
type: "verificationCodeTotp",
|
||||
name: "TOTP",
|
||||
},
|
||||
};
|
||||
const { value, type, name } = copyParams[this.loadAction as CopyAction];
|
||||
loadActionSuccess = await this.copy(value, type, name);
|
||||
}
|
||||
|
||||
if (this.inPopout) {
|
||||
setTimeout(() => this.close(), loadActionSuccess ? 1000 : 0);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
<header>
|
||||
<div class="left">
|
||||
<button type="button" routerLink="/tabs/settings">
|
||||
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
|
||||
<span>{{ "back" | i18n }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<h1 class="center">
|
||||
<span class="title">{{ "appearance" | i18n }}</span>
|
||||
</h1>
|
||||
<div class="right">
|
||||
<app-pop-out></app-pop-out>
|
||||
</div>
|
||||
</header>
|
||||
<main tabindex="-1">
|
||||
<div class="box">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="theme">{{ "theme" | i18n }}</label>
|
||||
<select
|
||||
id="theme"
|
||||
name="Theme"
|
||||
aria-describedby="themeHelp"
|
||||
[(ngModel)]="theme"
|
||||
(change)="saveTheme()"
|
||||
>
|
||||
<option *ngFor="let o of themeOptions" [ngValue]="o.value">{{ o.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="themeHelp" class="box-footer">
|
||||
{{ accountSwitcherEnabled ? ("themeDescAlt" | i18n) : ("themeDesc" | i18n) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||
<label for="badge">{{ "enableBadgeCounter" | i18n }}</label>
|
||||
<input
|
||||
id="badge"
|
||||
type="checkbox"
|
||||
aria-describedby="badgeHelp"
|
||||
(change)="updateBadgeCounter()"
|
||||
[(ngModel)]="enableBadgeCounter"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div id="badgeHelp" class="box-footer">{{ "badgeCounterDesc" | i18n }}</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||
<label for="favicon">{{ "enableFavicon" | i18n }}</label>
|
||||
<input
|
||||
id="favicon"
|
||||
type="checkbox"
|
||||
aria-describedby="faviconHelp"
|
||||
(change)="updateFavicon()"
|
||||
[(ngModel)]="enableFavicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div id="faviconHelp" class="box-footer">
|
||||
{{ accountSwitcherEnabled ? ("faviconDescAlt" | i18n) : ("faviconDesc" | i18n) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||
<label for="routing">{{ "enableAnimations" | i18n }}</label>
|
||||
<input
|
||||
id="routing"
|
||||
type="checkbox"
|
||||
(change)="updateRoutingAnimation()"
|
||||
[(ngModel)]="enableRoutingAnimation"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
@ -1,75 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service";
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import { AnimationControlService } from "@bitwarden/common/platform/abstractions/animation-control.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
|
||||
import { enableAccountSwitching } from "../../../platform/flags";
|
||||
|
||||
@Component({
|
||||
selector: "vault-appearance",
|
||||
templateUrl: "appearance.component.html",
|
||||
})
|
||||
export class AppearanceComponent implements OnInit {
|
||||
enableFavicon = false;
|
||||
enableBadgeCounter = true;
|
||||
theme: ThemeType;
|
||||
themeOptions: any[];
|
||||
accountSwitcherEnabled = false;
|
||||
enableRoutingAnimation: boolean;
|
||||
|
||||
constructor(
|
||||
private messagingService: MessagingService,
|
||||
private domainSettingsService: DomainSettingsService,
|
||||
private badgeSettingsService: BadgeSettingsServiceAbstraction,
|
||||
i18nService: I18nService,
|
||||
private themeStateService: ThemeStateService,
|
||||
private animationControlService: AnimationControlService,
|
||||
) {
|
||||
this.themeOptions = [
|
||||
{ name: i18nService.t("default"), value: ThemeType.System },
|
||||
{ name: i18nService.t("light"), value: ThemeType.Light },
|
||||
{ name: i18nService.t("dark"), value: ThemeType.Dark },
|
||||
{ name: "Nord", value: ThemeType.Nord },
|
||||
{ name: i18nService.t("solarizedDark"), value: ThemeType.SolarizedDark },
|
||||
];
|
||||
|
||||
this.accountSwitcherEnabled = enableAccountSwitching();
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.enableRoutingAnimation = await firstValueFrom(
|
||||
this.animationControlService.enableRoutingAnimation$,
|
||||
);
|
||||
|
||||
this.enableFavicon = await firstValueFrom(this.domainSettingsService.showFavicons$);
|
||||
|
||||
this.enableBadgeCounter = await firstValueFrom(this.badgeSettingsService.enableBadgeCounter$);
|
||||
|
||||
this.theme = await firstValueFrom(this.themeStateService.selectedTheme$);
|
||||
}
|
||||
|
||||
async updateRoutingAnimation() {
|
||||
await this.animationControlService.setEnableRoutingAnimation(this.enableRoutingAnimation);
|
||||
}
|
||||
|
||||
async updateFavicon() {
|
||||
await this.domainSettingsService.setShowFavicons(this.enableFavicon);
|
||||
}
|
||||
|
||||
async updateBadgeCounter() {
|
||||
await this.badgeSettingsService.setEnableBadgeCounter(this.enableBadgeCounter);
|
||||
this.messagingService.send("bgUpdateContextMenu");
|
||||
}
|
||||
|
||||
async saveTheme() {
|
||||
await this.themeStateService.setSelectedTheme(this.theme);
|
||||
}
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" [formGroup]="formGroup">
|
||||
<header>
|
||||
<div class="left">
|
||||
<button type="button" routerLink="/folders">{{ "cancel" | i18n }}</button>
|
||||
</div>
|
||||
<h1 class="center">
|
||||
<span class="title">{{ title }}</span>
|
||||
</h1>
|
||||
<div class="right">
|
||||
<button type="submit" [disabled]="form.loading">
|
||||
<span [hidden]="form.loading">{{ "save" | i18n }}</span>
|
||||
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<main tabindex="-1" *ngIf="folder">
|
||||
<div class="box">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="name">{{ "name" | i18n }}</label>
|
||||
<input id="name" type="text" formControlName="name" [appAutofocus]="!editMode" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box list" *ngIf="editMode">
|
||||
<div class="box-content single-line">
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row"
|
||||
appStopClick
|
||||
(click)="delete()"
|
||||
[appApiAction]="deletePromise"
|
||||
#deleteBtn
|
||||
>
|
||||
<div class="row-main text-danger">
|
||||
<div class="icon text-danger" aria-hidden="true">
|
||||
<i class="bwi bwi-trash bwi-lg bwi-fw" [hidden]="$any(deleteBtn).loading"></i>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
|
||||
[hidden]="!$any(deleteBtn).loading"
|
||||
></i>
|
||||
</div>
|
||||
<span>{{ "deleteFolder" | i18n }}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</form>
|
@ -1,78 +0,0 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { FolderAddEditComponent as BaseFolderAddEditComponent } from "@bitwarden/angular/vault/components/folder-add-edit.component";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@Component({
|
||||
selector: "app-folder-add-edit",
|
||||
templateUrl: "folder-add-edit.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class FolderAddEditComponent extends BaseFolderAddEditComponent implements OnInit {
|
||||
constructor(
|
||||
folderService: FolderService,
|
||||
folderApiService: FolderApiServiceAbstraction,
|
||||
accountService: AccountService,
|
||||
keyService: KeyService,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
logService: LogService,
|
||||
dialogService: DialogService,
|
||||
formBuilder: FormBuilder,
|
||||
) {
|
||||
super(
|
||||
folderService,
|
||||
folderApiService,
|
||||
accountService,
|
||||
keyService,
|
||||
i18nService,
|
||||
platformUtilsService,
|
||||
logService,
|
||||
dialogService,
|
||||
formBuilder,
|
||||
);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.queryParams.pipe(first()).subscribe(async (params) => {
|
||||
if (params.folderId) {
|
||||
this.folderId = params.folderId;
|
||||
}
|
||||
await this.init();
|
||||
});
|
||||
}
|
||||
|
||||
async submit(): Promise<boolean> {
|
||||
if (await super.submit()) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/folders"]);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async delete(): Promise<boolean> {
|
||||
const confirmed = await super.delete();
|
||||
if (confirmed) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/folders"]);
|
||||
}
|
||||
return confirmed;
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
<header>
|
||||
<div class="left">
|
||||
<button type="button" routerLink="/vault-settings">
|
||||
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
|
||||
<span>{{ "back" | i18n }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<h1 class="center">
|
||||
<span class="title">{{ "folders" | i18n }}</span>
|
||||
</h1>
|
||||
<div class="right">
|
||||
<button type="button" (click)="addFolder()" appA11yTitle="{{ 'addFolder' | i18n }}">
|
||||
<i class="bwi bwi-plus bwi-lg bwi-fw" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<main tabindex="-1">
|
||||
<ng-container *ngIf="folders$ | async as folders">
|
||||
<div class="box list full-list" *ngIf="folders.length; else noFoldersTemplate">
|
||||
<div class="box-content">
|
||||
<button
|
||||
type="button"
|
||||
appStopClick
|
||||
(click)="folderSelected(f)"
|
||||
class="box-content-row padded"
|
||||
*ngFor="let f of folders"
|
||||
>
|
||||
{{ f.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #noFoldersTemplate>
|
||||
<div class="no-items">
|
||||
<p>{{ "noFolders" | i18n }}</p>
|
||||
</div>
|
||||
</ng-template>
|
||||
</main>
|
@ -1,48 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { filter, map, Observable, switchMap } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
@Component({
|
||||
selector: "app-folders",
|
||||
templateUrl: "folders.component.html",
|
||||
})
|
||||
export class FoldersComponent {
|
||||
folders$: Observable<FolderView[]>;
|
||||
|
||||
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
|
||||
|
||||
constructor(
|
||||
private folderService: FolderService,
|
||||
private router: Router,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
this.folders$ = this.activeUserId$.pipe(
|
||||
filter((userId): userId is UserId => userId != null),
|
||||
switchMap((userId) => this.folderService.folderViews$(userId)),
|
||||
map((folders) => {
|
||||
// Remove the last folder, which is the "no folder" option folder
|
||||
if (folders.length > 0) {
|
||||
return folders.slice(0, folders.length - 1);
|
||||
}
|
||||
return folders;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
folderSelected(folder: FolderView) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/edit-folder"], { queryParams: { folderId: folder.id } });
|
||||
}
|
||||
|
||||
addFolder() {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/add-folder"]);
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
<header>
|
||||
<div class="left">
|
||||
<button type="button" routerLink="/vault-settings">
|
||||
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
|
||||
<span>{{ "back" | i18n }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<h1 class="center">
|
||||
<span class="title">{{ "sync" | i18n }}</span>
|
||||
</h1>
|
||||
<div class="right"></div>
|
||||
</header>
|
||||
<main tabindex="-1">
|
||||
<div class="content center-content">
|
||||
<button
|
||||
type="button"
|
||||
class="btn block primary"
|
||||
aria-describedby="lastSyncHint"
|
||||
(click)="sync()"
|
||||
#syncBtn
|
||||
[disabled]="$any(syncBtn).loading"
|
||||
[appApiAction]="syncPromise"
|
||||
>
|
||||
<span [hidden]="$any(syncBtn).loading">{{ "syncVaultNow" | i18n }}</span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-lg bwi-spin"
|
||||
[hidden]="!$any(syncBtn).loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
<p id="lastSyncHint" class="text-center text-muted small">
|
||||
{{ "lastSync" | i18n }} {{ lastSync }}
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
@ -1,46 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
|
||||
@Component({
|
||||
selector: "app-sync",
|
||||
templateUrl: "sync.component.html",
|
||||
})
|
||||
export class SyncComponent implements OnInit {
|
||||
lastSync = "--";
|
||||
syncPromise: Promise<any>;
|
||||
|
||||
constructor(
|
||||
private syncService: SyncService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.setLastSync();
|
||||
}
|
||||
|
||||
async sync() {
|
||||
this.syncPromise = this.syncService.fullSync(true);
|
||||
const success = await this.syncPromise;
|
||||
if (success) {
|
||||
await this.setLastSync();
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("syncingComplete"));
|
||||
} else {
|
||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("syncingFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
async setLastSync() {
|
||||
const last = await this.syncService.getLastSync();
|
||||
if (last != null) {
|
||||
this.lastSync = last.toLocaleDateString() + " " + last.toLocaleTimeString();
|
||||
} else {
|
||||
this.lastSync = this.i18nService.t("never");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
<app-header>
|
||||
<div class="left">
|
||||
<button type="button" routerLink="/tabs/settings">
|
||||
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
|
||||
<span>{{ "back" | i18n }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<h1 class="center">
|
||||
<span class="title">{{ "vault" | i18n }}</span>
|
||||
</h1>
|
||||
<div class="right">
|
||||
<app-pop-out></app-pop-out>
|
||||
</div>
|
||||
</app-header>
|
||||
<main tabindex="-1">
|
||||
<div class="box list">
|
||||
<div class="box-content single-line">
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row box-content-row-flex text-default"
|
||||
routerLink="/folders"
|
||||
>
|
||||
<div class="row-main">{{ "folders" | i18n }}</div>
|
||||
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row box-content-row-flex text-default"
|
||||
appStopClick
|
||||
(click)="import()"
|
||||
>
|
||||
<div class="row-main">{{ "importItems" | i18n }}</div>
|
||||
<i
|
||||
class="bwi bwi-external-link bwi-lg row-sub-icon bwi-rotate-270 bwi-fw"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row box-content-row-flex text-default"
|
||||
routerLink="/export"
|
||||
>
|
||||
<div class="row-main">{{ "exportVault" | i18n }}</div>
|
||||
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row box-content-row-flex text-default"
|
||||
routerLink="/sync"
|
||||
>
|
||||
<div class="row-main">{{ "sync" | i18n }}</div>
|
||||
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
@ -1,25 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
|
||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||
|
||||
@Component({
|
||||
selector: "vault-settings",
|
||||
templateUrl: "vault-settings.component.html",
|
||||
})
|
||||
export class VaultSettingsComponent {
|
||||
constructor(
|
||||
public messagingService: MessagingService,
|
||||
private router: Router,
|
||||
) {}
|
||||
|
||||
async import() {
|
||||
await this.router.navigate(["/import"]);
|
||||
if (await BrowserApi.isPopupOpen()) {
|
||||
await BrowserPopupUtils.openCurrentPagePopout(window);
|
||||
}
|
||||
}
|
||||
}
|
16
apps/desktop/desktop_native/Cargo.lock
generated
16
apps/desktop/desktop_native/Cargo.lock
generated
@ -567,9 +567,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.4"
|
||||
version = "1.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9157bbaa6b165880c27a4293a474c91cdcf265cc68cc829bf10be0964a391caf"
|
||||
checksum = "8d6dbb628b8f8555f86d0323c2eb39e3ec81901f4b83e091db8a6a76d316a333"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
@ -1134,7 +1134,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1519,7 +1519,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.52.6",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1667,9 +1667,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "napi-build"
|
||||
version = "2.1.3"
|
||||
version = "2.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1c0f5d67ee408a4685b61f5ab7e58605c8ae3f2b4189f0127d804ff13d5560a"
|
||||
checksum = "db836caddef23662b94e16bf1f26c40eceb09d6aee5d5b06a7ac199320b69b19"
|
||||
|
||||
[[package]]
|
||||
name = "napi-derive"
|
||||
@ -2836,7 +2836,7 @@ dependencies = [
|
||||
"fastrand",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3340,7 +3340,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -21,10 +21,10 @@ serde = { version = "1.0.205", features = ["derive"] }
|
||||
serde_json = "1.0.122"
|
||||
tokio = { version = "1.39.2", features = ["sync"] }
|
||||
tokio-util = "0.7.11"
|
||||
uniffi = { version = "0.28.0", features = ["cli"] }
|
||||
uniffi = { version = "0.28.3", features = ["cli"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
oslog = "0.2.0"
|
||||
|
||||
[build-dependencies]
|
||||
uniffi = { version = "0.28.0", features = ["build"] }
|
||||
uniffi = { version = "0.28.3", features = ["build"] }
|
||||
|
@ -30,4 +30,4 @@ tokio-stream = "=0.1.15"
|
||||
windows-registry = "=0.3.0"
|
||||
|
||||
[build-dependencies]
|
||||
napi-build = "=2.1.3"
|
||||
napi-build = "=2.1.4"
|
||||
|
@ -17,5 +17,5 @@ tokio = "1.39.1"
|
||||
core-foundation = "=0.10.0"
|
||||
|
||||
[build-dependencies]
|
||||
cc = "1.0.104"
|
||||
cc = "1.2.4"
|
||||
glob = "0.3.1"
|
||||
|
@ -3399,10 +3399,10 @@
|
||||
"message": "ملاحظة هامة"
|
||||
},
|
||||
"setupTwoStepLogin": {
|
||||
"message": "إعداد المصادقة الثنائية"
|
||||
"message": "إعداد تسجيل الدخول بخطوتين"
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage1": {
|
||||
"message": "سيقوم Bitwarden بإرسال رمز إلى البريد الإلكتروني الخاص بحسابك للتحقق من تسجيلات الدخول من الأجهزة الجديدة ابتداء من فبراير 2025."
|
||||
"message": "سيقوم Bitwarden بإرسال رمز إلى البريد الإلكتروني الخاص بحسابك للتحقق من تسجيلات الدخول من الأجهزة الجديدة ابتداءً من فبراير 2025."
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage2": {
|
||||
"message": "يمكنك إعداد المصادقة الثنائية كطريقة بديلة لحماية حسابك أو تغيير بريدك الإلكتروني إلى بريد يمكنك الوصول إليه."
|
||||
@ -3426,7 +3426,7 @@
|
||||
"message": "نعم، يمكنني الوصول بشكل موثوق إلى بريدي الإلكتروني"
|
||||
},
|
||||
"turnOnTwoStepLogin": {
|
||||
"message": "تفعيل المصادقة الثنائية"
|
||||
"message": "تشغيل تسجيل الدخول بخطوتين"
|
||||
},
|
||||
"changeAcctEmail": {
|
||||
"message": "تغيير البريد الإلكتروني الخاص بالحساب"
|
||||
|
@ -2729,7 +2729,7 @@
|
||||
"message": "Laitteeseesi lähetettiin ilmoitus"
|
||||
},
|
||||
"makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
|
||||
"message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
|
||||
"message": "Varmista, että vahvistavan laitteen holvi on avattu ja että se näyttää saman tunnistelausekkeen"
|
||||
},
|
||||
"needAnotherOptionV1": {
|
||||
"message": "Tarvitsetko toisen vaihtoehdon?"
|
||||
@ -3402,7 +3402,7 @@
|
||||
"message": "Määritä kaksivaiheinen kirjautuminen"
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage1": {
|
||||
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025."
|
||||
"message": "Bitwarden lähettää tilisi sähköpostiosoitteeseen koodin, jolla voit vahvistaa kirjautumiset uusista laitteista helmikuusta 2025 alkaen."
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage2": {
|
||||
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access."
|
||||
|
@ -323,7 +323,7 @@
|
||||
"message": "Générer un mot de passe"
|
||||
},
|
||||
"generatePassphrase": {
|
||||
"message": "Generate passphrase"
|
||||
"message": "Générer une phrase de passe"
|
||||
},
|
||||
"type": {
|
||||
"message": "Type"
|
||||
@ -467,7 +467,7 @@
|
||||
"message": "Copier la clé privée SSH"
|
||||
},
|
||||
"copyPassphrase": {
|
||||
"message": "Copy passphrase",
|
||||
"message": "Copier la phrase de passe",
|
||||
"description": "Copy passphrase to clipboard"
|
||||
},
|
||||
"copyUri": {
|
||||
@ -926,7 +926,7 @@
|
||||
"message": "La session d'authentification a expiré. Veuillez redémarrer le processus de connexion."
|
||||
},
|
||||
"selfHostBaseUrl": {
|
||||
"message": "Self-host server URL",
|
||||
"message": "URL du serveur auto-hébergé",
|
||||
"description": "Label for field requesting a self-hosted integration service URL"
|
||||
},
|
||||
"apiUrl": {
|
||||
@ -1951,7 +1951,7 @@
|
||||
"message": "Votre nouveau mot de passe principal ne répond pas aux exigences de politique de sécurité."
|
||||
},
|
||||
"receiveMarketingEmailsV2": {
|
||||
"message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox."
|
||||
"message": "Obtenez des conseils, des annonces et des opportunités de recherche de la part de Bitwarden dans votre boîte de réception."
|
||||
},
|
||||
"unsubscribe": {
|
||||
"message": "Se désabonner"
|
||||
@ -2478,10 +2478,10 @@
|
||||
"message": "Générer le nom d'utilisateur"
|
||||
},
|
||||
"generateEmail": {
|
||||
"message": "Generate email"
|
||||
"message": "Générer un courriel"
|
||||
},
|
||||
"spinboxBoundariesHint": {
|
||||
"message": "Value must be between $MIN$ and $MAX$.",
|
||||
"message": "La valeur doit être comprise entre $MIN$ et $MAX$.",
|
||||
"description": "Explains spin box minimum and maximum values to the user",
|
||||
"placeholders": {
|
||||
"min": {
|
||||
@ -2495,7 +2495,7 @@
|
||||
}
|
||||
},
|
||||
"passwordLengthRecommendationHint": {
|
||||
"message": " Use $RECOMMENDED$ characters or more to generate a strong password.",
|
||||
"message": " Utilisez $RECOMMENDED$ caractères ou plus pour générer un mot de passe fort.",
|
||||
"description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
|
||||
"placeholders": {
|
||||
"recommended": {
|
||||
@ -2505,7 +2505,7 @@
|
||||
}
|
||||
},
|
||||
"passphraseNumWordsRecommendationHint": {
|
||||
"message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.",
|
||||
"message": " Utilisez $RECOMMENDED$ mots ou plus pour générer une phrase de passe forte.",
|
||||
"description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
|
||||
"placeholders": {
|
||||
"recommended": {
|
||||
@ -2729,10 +2729,10 @@
|
||||
"message": "A notification was sent to your device"
|
||||
},
|
||||
"makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
|
||||
"message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
|
||||
"message": "Assurez-vous que votre compte est déverrouillé et que la phrase d'empreinte digitale correspond à celle de l'autre appareil"
|
||||
},
|
||||
"needAnotherOptionV1": {
|
||||
"message": "Need another option?"
|
||||
"message": "Besoin d'une autre option ?"
|
||||
},
|
||||
"fingerprintMatchInfo": {
|
||||
"message": "Veuillez vous assurer que votre coffre est déverrouillé et que la phrase d'empreinte correspond à celle de l'autre appareil."
|
||||
@ -2741,13 +2741,13 @@
|
||||
"message": "Phrase d'empreinte"
|
||||
},
|
||||
"youWillBeNotifiedOnceTheRequestIsApproved": {
|
||||
"message": "You will be notified once the request is approved"
|
||||
"message": "Vous serez notifié une fois que la demande sera approuvée"
|
||||
},
|
||||
"needAnotherOption": {
|
||||
"message": "La connexion avec l'appareil doit être configurée dans les paramètres de l'application Bitwarden. Besoin d'une autre option ?"
|
||||
},
|
||||
"viewAllLogInOptions": {
|
||||
"message": "View all log in options"
|
||||
"message": "Afficher toutes les options de connexion"
|
||||
},
|
||||
"viewAllLoginOptions": {
|
||||
"message": "Afficher toutes les options de connexion"
|
||||
|
@ -27,7 +27,7 @@
|
||||
"message": "Nota sicura"
|
||||
},
|
||||
"typeSshKey": {
|
||||
"message": "SSH key"
|
||||
"message": "Chiave SSH"
|
||||
},
|
||||
"folders": {
|
||||
"message": "Cartelle"
|
||||
@ -64,7 +64,7 @@
|
||||
}
|
||||
},
|
||||
"welcomeBack": {
|
||||
"message": "Welcome back"
|
||||
"message": "Bentornato"
|
||||
},
|
||||
"moveToOrgDesc": {
|
||||
"message": "Scegli un'organizzazione in cui vuoi spostare questo elemento. Spostarlo in un'organizzazione trasferisce la proprietà dell'elemento all'organizzazione. Una volta spostato, non sarai più il proprietario diretto di questo elemento."
|
||||
@ -181,61 +181,61 @@
|
||||
"message": "Indirizzo"
|
||||
},
|
||||
"sshPrivateKey": {
|
||||
"message": "Private key"
|
||||
"message": "Chiave privata"
|
||||
},
|
||||
"sshPublicKey": {
|
||||
"message": "Public key"
|
||||
"message": "Chiave pubblica"
|
||||
},
|
||||
"sshFingerprint": {
|
||||
"message": "Fingerprint"
|
||||
"message": "Impronta digitale"
|
||||
},
|
||||
"sshKeyAlgorithm": {
|
||||
"message": "Key type"
|
||||
"message": "Tipo di chiave"
|
||||
},
|
||||
"sshKeyAlgorithmED25519": {
|
||||
"message": "ED25519"
|
||||
},
|
||||
"sshKeyAlgorithmRSA2048": {
|
||||
"message": "RSA 2048-Bit"
|
||||
"message": "RSA a 2048 bit"
|
||||
},
|
||||
"sshKeyAlgorithmRSA3072": {
|
||||
"message": "RSA 3072-Bit"
|
||||
"message": "RSA a 3072 bit"
|
||||
},
|
||||
"sshKeyAlgorithmRSA4096": {
|
||||
"message": "RSA 4096-Bit"
|
||||
"message": "RSA a 4096 bit"
|
||||
},
|
||||
"sshKeyGenerated": {
|
||||
"message": "A new SSH key was generated"
|
||||
"message": "È stata generata una nuova chiave SSH"
|
||||
},
|
||||
"sshKeyWrongPassword": {
|
||||
"message": "The password you entered is incorrect."
|
||||
"message": "La password inserita non è corretta."
|
||||
},
|
||||
"importSshKey": {
|
||||
"message": "Import"
|
||||
"message": "Importa"
|
||||
},
|
||||
"confirmSshKeyPassword": {
|
||||
"message": "Confirm password"
|
||||
"message": "Conferma password"
|
||||
},
|
||||
"enterSshKeyPasswordDesc": {
|
||||
"message": "Enter the password for the SSH key."
|
||||
"message": "Inserisci la password per la chiave SSH."
|
||||
},
|
||||
"enterSshKeyPassword": {
|
||||
"message": "Enter password"
|
||||
"message": "Inserisci password"
|
||||
},
|
||||
"sshAgentUnlockRequired": {
|
||||
"message": "Please unlock your vault to approve the SSH key request."
|
||||
"message": "Sbloccare la cassaforte per approvare la richiesta di chiave SSH."
|
||||
},
|
||||
"sshAgentUnlockTimeout": {
|
||||
"message": "SSH key request timed out."
|
||||
"message": "Richiesta chiave SSH scaduta."
|
||||
},
|
||||
"enableSshAgent": {
|
||||
"message": "Enable SSH agent"
|
||||
"message": "Abilita agente SSH"
|
||||
},
|
||||
"enableSshAgentDesc": {
|
||||
"message": "Enable the SSH agent to sign SSH requests right from your Bitwarden vault."
|
||||
"message": "Abilita l'agente SSH per firmare le richieste SSH direttamente dalla tua cassaforte Bitwarden."
|
||||
},
|
||||
"enableSshAgentHelp": {
|
||||
"message": "The SSH agent is a service targeted at developers that allows you to sign SSH requests directly from your Bitwarden vault."
|
||||
"message": "L'agente SSH è un servizio rivolto agli sviluppatori che consente di firmare le richieste SSH direttamente dalla tua cassaforte Bitwarden."
|
||||
},
|
||||
"premiumRequired": {
|
||||
"message": "Premium necessario"
|
||||
@ -461,10 +461,10 @@
|
||||
"message": "Copia password"
|
||||
},
|
||||
"regenerateSshKey": {
|
||||
"message": "Regenerate SSH key"
|
||||
"message": "Rigenera la chiave SSH"
|
||||
},
|
||||
"copySshPrivateKey": {
|
||||
"message": "Copy SSH private key"
|
||||
"message": "Copia chiave privata SSH"
|
||||
},
|
||||
"copyPassphrase": {
|
||||
"message": "Copia passphrase",
|
||||
@ -624,7 +624,7 @@
|
||||
"message": "Crea account"
|
||||
},
|
||||
"newToBitwarden": {
|
||||
"message": "New to Bitwarden?"
|
||||
"message": "Nuovo in Bitwarden?"
|
||||
},
|
||||
"setAStrongPassword": {
|
||||
"message": "Imposta una password robusta"
|
||||
@ -636,16 +636,16 @@
|
||||
"message": "Accedi"
|
||||
},
|
||||
"logInToBitwarden": {
|
||||
"message": "Log in to Bitwarden"
|
||||
"message": "Accedi a Bitwarden"
|
||||
},
|
||||
"logInWithPasskey": {
|
||||
"message": "Log in with passkey"
|
||||
"message": "Accedi con passkey"
|
||||
},
|
||||
"loginWithDevice": {
|
||||
"message": "Log in with device"
|
||||
"message": "Accedi con dispositivo"
|
||||
},
|
||||
"useSingleSignOn": {
|
||||
"message": "Use single sign-on"
|
||||
"message": "Usa il Single Sign-On"
|
||||
},
|
||||
"submit": {
|
||||
"message": "Invia"
|
||||
@ -920,10 +920,10 @@
|
||||
"message": "URL del server"
|
||||
},
|
||||
"authenticationTimeout": {
|
||||
"message": "Authentication timeout"
|
||||
"message": "Timeout autenticazione"
|
||||
},
|
||||
"authenticationSessionTimedOut": {
|
||||
"message": "The authentication session timed out. Please restart the login process."
|
||||
"message": "La sessione di autenticazione è scaduta. Accedi di nuovo."
|
||||
},
|
||||
"selfHostBaseUrl": {
|
||||
"message": "URL server autogestito",
|
||||
@ -1393,13 +1393,13 @@
|
||||
"message": "Cronologia delle password"
|
||||
},
|
||||
"generatorHistory": {
|
||||
"message": "Generator history"
|
||||
"message": "Cronologia generatore"
|
||||
},
|
||||
"clearGeneratorHistoryTitle": {
|
||||
"message": "Clear generator history"
|
||||
"message": "Cancella cronologia generatore"
|
||||
},
|
||||
"cleargGeneratorHistoryDescription": {
|
||||
"message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?"
|
||||
"message": "Se continui, tutte le voci verranno eliminate definitivamente dalla cronologia del generatore. Vuoi continuare?"
|
||||
},
|
||||
"clear": {
|
||||
"message": "Cancella",
|
||||
@ -1409,13 +1409,13 @@
|
||||
"message": "Non ci sono password da mostrare."
|
||||
},
|
||||
"clearHistory": {
|
||||
"message": "Clear history"
|
||||
"message": "Cancella cronologia"
|
||||
},
|
||||
"nothingToShow": {
|
||||
"message": "Nothing to show"
|
||||
"message": "Niente da mostrare"
|
||||
},
|
||||
"nothingGeneratedRecently": {
|
||||
"message": "You haven't generated anything recently"
|
||||
"message": "Non hai generato niente di recente"
|
||||
},
|
||||
"undo": {
|
||||
"message": "Annulla"
|
||||
@ -1771,10 +1771,10 @@
|
||||
"message": "L'eliminazione del tuo account è permanente. Non può essere annullata."
|
||||
},
|
||||
"cannotDeleteAccount": {
|
||||
"message": "Cannot delete account"
|
||||
"message": "Impossibile eliminare account"
|
||||
},
|
||||
"cannotDeleteAccountDesc": {
|
||||
"message": "This action cannot be completed because your account is owned by an organization. Contact your organization administrator for additional details."
|
||||
"message": "Questa azione non può essere completata perché il tuo account è di proprietà di un'organizzazione. Contatta l'amministratore della tua organizzazione per dettagli."
|
||||
},
|
||||
"accountDeleted": {
|
||||
"message": "Account eliminato"
|
||||
@ -2481,7 +2481,7 @@
|
||||
"message": "Genera e-mail"
|
||||
},
|
||||
"spinboxBoundariesHint": {
|
||||
"message": "Value must be between $MIN$ and $MAX$.",
|
||||
"message": "Il valore deve essere compreso tra $MIN$ e $MAX$.",
|
||||
"description": "Explains spin box minimum and maximum values to the user",
|
||||
"placeholders": {
|
||||
"min": {
|
||||
@ -2495,7 +2495,7 @@
|
||||
}
|
||||
},
|
||||
"passwordLengthRecommendationHint": {
|
||||
"message": " Use $RECOMMENDED$ characters or more to generate a strong password.",
|
||||
"message": " Usa $RECOMMENDED$ caratteri o più per generare una password forte.",
|
||||
"description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
|
||||
"placeholders": {
|
||||
"recommended": {
|
||||
@ -2505,7 +2505,7 @@
|
||||
}
|
||||
},
|
||||
"passphraseNumWordsRecommendationHint": {
|
||||
"message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.",
|
||||
"message": " Usa $RECOMMENDED$ parole o più per generare una passphrase forte.",
|
||||
"description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
|
||||
"placeholders": {
|
||||
"recommended": {
|
||||
@ -2726,13 +2726,13 @@
|
||||
"message": "Una notifica è stata inviata al tuo dispositivo."
|
||||
},
|
||||
"aNotificationWasSentToYourDevice": {
|
||||
"message": "A notification was sent to your device"
|
||||
"message": "Una notifica è stata inviata al tuo dispositivo"
|
||||
},
|
||||
"makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
|
||||
"message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
|
||||
"message": "Assicurati che il tuo account sia sbloccato e che la frase dell'impronta digitale corrisponda nell'altro dispositivo"
|
||||
},
|
||||
"needAnotherOptionV1": {
|
||||
"message": "Need another option?"
|
||||
"message": "Bisogno di un'altra opzione?"
|
||||
},
|
||||
"fingerprintMatchInfo": {
|
||||
"message": "Assicurati che la tua cassaforte sia sbloccata e che la frase impronta corrisponda sull'altro dispositivo."
|
||||
@ -2741,13 +2741,13 @@
|
||||
"message": "Frase impronta"
|
||||
},
|
||||
"youWillBeNotifiedOnceTheRequestIsApproved": {
|
||||
"message": "You will be notified once the request is approved"
|
||||
"message": "Sarai notificato una volta che la richiesta sarà approvata"
|
||||
},
|
||||
"needAnotherOption": {
|
||||
"message": "L'accesso con dispositivo deve essere abilitato nelle impostazioni dell'app Bitwarden. Ti serve un'altra opzione?"
|
||||
},
|
||||
"viewAllLogInOptions": {
|
||||
"message": "View all log in options"
|
||||
"message": "Visualizza tutte le opzioni di accesso"
|
||||
},
|
||||
"viewAllLoginOptions": {
|
||||
"message": "Visualizza tutte le opzioni di accesso"
|
||||
@ -2869,7 +2869,7 @@
|
||||
"message": "Controlla se la tua password è presente in una violazione dei dati"
|
||||
},
|
||||
"loggedInExclamation": {
|
||||
"message": "Logged in!"
|
||||
"message": "Accesso effettuato!"
|
||||
},
|
||||
"important": {
|
||||
"message": "Importante:"
|
||||
@ -2902,16 +2902,16 @@
|
||||
"message": "Aggiornamento delle impostazioni consigliato"
|
||||
},
|
||||
"rememberThisDeviceToMakeFutureLoginsSeamless": {
|
||||
"message": "Remember this device to make future logins seamless"
|
||||
"message": "Ricorda questo dispositivo per rendere immediati i futuri accessi"
|
||||
},
|
||||
"deviceApprovalRequired": {
|
||||
"message": "Approvazione del dispositivo obbligatoria. Seleziona un'opzione di approvazione:"
|
||||
},
|
||||
"deviceApprovalRequiredV2": {
|
||||
"message": "Device approval required"
|
||||
"message": "Approvazione dispositivo richiesta"
|
||||
},
|
||||
"selectAnApprovalOptionBelow": {
|
||||
"message": "Select an approval option below"
|
||||
"message": "Seleziona un'opzione di approvazione sotto"
|
||||
},
|
||||
"rememberThisDevice": {
|
||||
"message": "Ricorda questo dispositivo"
|
||||
@ -2966,7 +2966,7 @@
|
||||
"message": "Email utente mancante"
|
||||
},
|
||||
"activeUserEmailNotFoundLoggingYouOut": {
|
||||
"message": "Active user email not found. Logging you out."
|
||||
"message": "Email utente attiva non trovata. Logout in corso."
|
||||
},
|
||||
"deviceTrusted": {
|
||||
"message": "Dispositivo fidato"
|
||||
@ -3363,55 +3363,55 @@
|
||||
"message": "Non è stato possibile trovare nessuna porta libera per il login Sso."
|
||||
},
|
||||
"authorize": {
|
||||
"message": "Authorize"
|
||||
"message": "Autorizza"
|
||||
},
|
||||
"deny": {
|
||||
"message": "Deny"
|
||||
"message": "Nega"
|
||||
},
|
||||
"sshkeyApprovalTitle": {
|
||||
"message": "Confirm SSH key usage"
|
||||
"message": "Conferma l'uso della chiave SSH"
|
||||
},
|
||||
"sshkeyApprovalMessageInfix": {
|
||||
"message": "is requesting access to"
|
||||
"message": "richiede l'accesso a"
|
||||
},
|
||||
"unknownApplication": {
|
||||
"message": "An application"
|
||||
"message": "Un'applicazione"
|
||||
},
|
||||
"sshKeyPasswordUnsupported": {
|
||||
"message": "Importing password protected SSH keys is not yet supported"
|
||||
"message": "L'importazione di chiavi SSH protette da password non è ancora supportata"
|
||||
},
|
||||
"invalidSshKey": {
|
||||
"message": "The SSH key is invalid"
|
||||
"message": "La chiave SSH non è valida"
|
||||
},
|
||||
"sshKeyTypeUnsupported": {
|
||||
"message": "The SSH key type is not supported"
|
||||
"message": "Il tipo di chiave SSH non è supportato"
|
||||
},
|
||||
"importSshKeyFromClipboard": {
|
||||
"message": "Import key from clipboard"
|
||||
"message": "Importa chiave dagli Appunti"
|
||||
},
|
||||
"sshKeyPasted": {
|
||||
"message": "SSH key imported successfully"
|
||||
"message": "Chiave SSH importata correttamente"
|
||||
},
|
||||
"fileSavedToDevice": {
|
||||
"message": "File salvato sul dispositivo. Gestisci dai download del dispositivo."
|
||||
},
|
||||
"importantNotice": {
|
||||
"message": "Important notice"
|
||||
"message": "Notifica importante"
|
||||
},
|
||||
"setupTwoStepLogin": {
|
||||
"message": "Set up two-step login"
|
||||
"message": "Imposta accesso in due passaggi"
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage1": {
|
||||
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025."
|
||||
"message": "Bitwarden invierà un codice all'e-mail del tuo account per verificare gli accessi da nuovi dispositivi a partire da febbraio 2025."
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage2": {
|
||||
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access."
|
||||
"message": "Puoi impostare l'accesso in due passaggi come modo alternativo per proteggere il tuo account, o cambiare la tua e-mail in una alla quale puoi accedere."
|
||||
},
|
||||
"remindMeLater": {
|
||||
"message": "Remind me later"
|
||||
"message": "Ricordamelo più tardi"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneFormContent": {
|
||||
"message": "Do you have reliable access to your email, $EMAIL$?",
|
||||
"message": "Hai accesso affidabile alla tua e-mail, $EMAIL$?",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
@ -3420,15 +3420,15 @@
|
||||
}
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessNo": {
|
||||
"message": "No, I do not"
|
||||
"message": "No, non ce l'ho"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessYes": {
|
||||
"message": "Yes, I can reliably access my email"
|
||||
"message": "Sì, posso accedere in modo affidabile alla mia e-mail"
|
||||
},
|
||||
"turnOnTwoStepLogin": {
|
||||
"message": "Turn on two-step login"
|
||||
"message": "Attiva accesso in due passaggi"
|
||||
},
|
||||
"changeAcctEmail": {
|
||||
"message": "Change account email"
|
||||
"message": "Cambia l'e-mail dell'account"
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,7 @@
|
||||
"message": "セキュアメモ"
|
||||
},
|
||||
"typeSshKey": {
|
||||
"message": "SSH key"
|
||||
"message": "SSH キー"
|
||||
},
|
||||
"folders": {
|
||||
"message": "フォルダー"
|
||||
@ -64,7 +64,7 @@
|
||||
}
|
||||
},
|
||||
"welcomeBack": {
|
||||
"message": "Welcome back"
|
||||
"message": "ようこそ"
|
||||
},
|
||||
"moveToOrgDesc": {
|
||||
"message": "このアイテムを移動する組織を選択してください。組織に移動すると、アイテムの所有権がその組織に移行します。 このアイテムが移動された後、あなたはこのアイテムの直接の所有者にはなりません。"
|
||||
@ -181,16 +181,16 @@
|
||||
"message": "住所"
|
||||
},
|
||||
"sshPrivateKey": {
|
||||
"message": "Private key"
|
||||
"message": "秘密鍵"
|
||||
},
|
||||
"sshPublicKey": {
|
||||
"message": "Public key"
|
||||
"message": "公開鍵"
|
||||
},
|
||||
"sshFingerprint": {
|
||||
"message": "Fingerprint"
|
||||
"message": "フィンガープリント"
|
||||
},
|
||||
"sshKeyAlgorithm": {
|
||||
"message": "Key type"
|
||||
"message": "キーの種類"
|
||||
},
|
||||
"sshKeyAlgorithmED25519": {
|
||||
"message": "ED25519"
|
||||
@ -205,37 +205,37 @@
|
||||
"message": "RSA 4096-Bit"
|
||||
},
|
||||
"sshKeyGenerated": {
|
||||
"message": "A new SSH key was generated"
|
||||
"message": "新しい SSH 鍵が生成されました"
|
||||
},
|
||||
"sshKeyWrongPassword": {
|
||||
"message": "The password you entered is incorrect."
|
||||
"message": "入力されたパスワードが間違っています。"
|
||||
},
|
||||
"importSshKey": {
|
||||
"message": "Import"
|
||||
"message": "インポート"
|
||||
},
|
||||
"confirmSshKeyPassword": {
|
||||
"message": "Confirm password"
|
||||
"message": "パスワードを確認"
|
||||
},
|
||||
"enterSshKeyPasswordDesc": {
|
||||
"message": "Enter the password for the SSH key."
|
||||
"message": "SSH キーのパスワードを入力します。"
|
||||
},
|
||||
"enterSshKeyPassword": {
|
||||
"message": "Enter password"
|
||||
"message": "パスワードを入力"
|
||||
},
|
||||
"sshAgentUnlockRequired": {
|
||||
"message": "Please unlock your vault to approve the SSH key request."
|
||||
"message": "SSH キーリクエストを承認するには、保管庫のロックを解除してください。"
|
||||
},
|
||||
"sshAgentUnlockTimeout": {
|
||||
"message": "SSH key request timed out."
|
||||
"message": "SSH キーの要求がタイムアウトしました。"
|
||||
},
|
||||
"enableSshAgent": {
|
||||
"message": "Enable SSH agent"
|
||||
"message": "SSH エージェントを有効にする"
|
||||
},
|
||||
"enableSshAgentDesc": {
|
||||
"message": "Enable the SSH agent to sign SSH requests right from your Bitwarden vault."
|
||||
"message": "Bitwarden 保管庫から直接 SSH 要求に署名するために SSH エージェントを有効にします。"
|
||||
},
|
||||
"enableSshAgentHelp": {
|
||||
"message": "The SSH agent is a service targeted at developers that allows you to sign SSH requests directly from your Bitwarden vault."
|
||||
"message": "SSH エージェントとは、Bitwarden 保管庫から直接 SSH リクエストに署名できる、開発者を対象としたサービスです。"
|
||||
},
|
||||
"premiumRequired": {
|
||||
"message": "プレミアム会員専用"
|
||||
@ -323,7 +323,7 @@
|
||||
"message": "パスワードの自動生成"
|
||||
},
|
||||
"generatePassphrase": {
|
||||
"message": "Generate passphrase"
|
||||
"message": "パスフレーズを生成"
|
||||
},
|
||||
"type": {
|
||||
"message": "タイプ"
|
||||
@ -461,13 +461,13 @@
|
||||
"message": "パスワードのコピー"
|
||||
},
|
||||
"regenerateSshKey": {
|
||||
"message": "Regenerate SSH key"
|
||||
"message": "SSH キーを再生成"
|
||||
},
|
||||
"copySshPrivateKey": {
|
||||
"message": "Copy SSH private key"
|
||||
"message": "SSH 秘密鍵をコピー"
|
||||
},
|
||||
"copyPassphrase": {
|
||||
"message": "Copy passphrase",
|
||||
"message": "パスフレーズをコピー",
|
||||
"description": "Copy passphrase to clipboard"
|
||||
},
|
||||
"copyUri": {
|
||||
@ -624,7 +624,7 @@
|
||||
"message": "アカウントの作成"
|
||||
},
|
||||
"newToBitwarden": {
|
||||
"message": "New to Bitwarden?"
|
||||
"message": "Bitwarden は初めてですか?"
|
||||
},
|
||||
"setAStrongPassword": {
|
||||
"message": "強力なパスワードを設定する"
|
||||
@ -636,16 +636,16 @@
|
||||
"message": "ログイン"
|
||||
},
|
||||
"logInToBitwarden": {
|
||||
"message": "Log in to Bitwarden"
|
||||
"message": "Bitwarden にログイン"
|
||||
},
|
||||
"logInWithPasskey": {
|
||||
"message": "Log in with passkey"
|
||||
"message": "パスキーでログイン"
|
||||
},
|
||||
"loginWithDevice": {
|
||||
"message": "Log in with device"
|
||||
"message": "デバイスでログイン"
|
||||
},
|
||||
"useSingleSignOn": {
|
||||
"message": "Use single sign-on"
|
||||
"message": "シングルサインオンを使用する"
|
||||
},
|
||||
"submit": {
|
||||
"message": "送信"
|
||||
@ -920,13 +920,13 @@
|
||||
"message": "サーバー URL"
|
||||
},
|
||||
"authenticationTimeout": {
|
||||
"message": "Authentication timeout"
|
||||
"message": "認証のタイムアウト"
|
||||
},
|
||||
"authenticationSessionTimedOut": {
|
||||
"message": "The authentication session timed out. Please restart the login process."
|
||||
"message": "認証セッションの有効期限が切れました。ログイン操作を最初からやり直してください。"
|
||||
},
|
||||
"selfHostBaseUrl": {
|
||||
"message": "Self-host server URL",
|
||||
"message": "自己ホスト型サーバーの URL",
|
||||
"description": "Label for field requesting a self-hosted integration service URL"
|
||||
},
|
||||
"apiUrl": {
|
||||
@ -1320,7 +1320,7 @@
|
||||
"description": "Copy credit card number"
|
||||
},
|
||||
"copyEmail": {
|
||||
"message": "Copy email"
|
||||
"message": "メールアドレスをコピー"
|
||||
},
|
||||
"copySecurityCode": {
|
||||
"message": "セキュリティコードのコピー",
|
||||
@ -1393,13 +1393,13 @@
|
||||
"message": "パスワードの履歴"
|
||||
},
|
||||
"generatorHistory": {
|
||||
"message": "Generator history"
|
||||
"message": "生成履歴"
|
||||
},
|
||||
"clearGeneratorHistoryTitle": {
|
||||
"message": "Clear generator history"
|
||||
"message": "生成履歴を消去"
|
||||
},
|
||||
"cleargGeneratorHistoryDescription": {
|
||||
"message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?"
|
||||
"message": "続行すると、すべてのエントリは生成履歴から完全に削除されます。続行してもよろしいですか?"
|
||||
},
|
||||
"clear": {
|
||||
"message": "消去する",
|
||||
@ -1409,13 +1409,13 @@
|
||||
"message": "表示するパスワードがありません"
|
||||
},
|
||||
"clearHistory": {
|
||||
"message": "Clear history"
|
||||
"message": "履歴を消去"
|
||||
},
|
||||
"nothingToShow": {
|
||||
"message": "Nothing to show"
|
||||
"message": "表示するものがありません"
|
||||
},
|
||||
"nothingGeneratedRecently": {
|
||||
"message": "You haven't generated anything recently"
|
||||
"message": "最近生成したものはありません"
|
||||
},
|
||||
"undo": {
|
||||
"message": "元に戻す"
|
||||
@ -1771,10 +1771,10 @@
|
||||
"message": "アカウントを恒久的に削除します。元に戻すことはできません。"
|
||||
},
|
||||
"cannotDeleteAccount": {
|
||||
"message": "Cannot delete account"
|
||||
"message": "アカウントを削除できません"
|
||||
},
|
||||
"cannotDeleteAccountDesc": {
|
||||
"message": "This action cannot be completed because your account is owned by an organization. Contact your organization administrator for additional details."
|
||||
"message": "このアカウントは組織が所有しているため、操作を完了できません。詳しくは組織の管理者へご確認ください。"
|
||||
},
|
||||
"accountDeleted": {
|
||||
"message": "アカウントが削除されました"
|
||||
@ -2478,10 +2478,10 @@
|
||||
"message": "ユーザー名を生成"
|
||||
},
|
||||
"generateEmail": {
|
||||
"message": "Generate email"
|
||||
"message": "メールアドレスを生成"
|
||||
},
|
||||
"spinboxBoundariesHint": {
|
||||
"message": "Value must be between $MIN$ and $MAX$.",
|
||||
"message": "値は $MIN$ から $MAX$ の間でなければなりません。",
|
||||
"description": "Explains spin box minimum and maximum values to the user",
|
||||
"placeholders": {
|
||||
"min": {
|
||||
@ -2495,7 +2495,7 @@
|
||||
}
|
||||
},
|
||||
"passwordLengthRecommendationHint": {
|
||||
"message": " Use $RECOMMENDED$ characters or more to generate a strong password.",
|
||||
"message": " 強力なパスワードを生成するには、 $RECOMMENDED$ 文字以上を使用してください。",
|
||||
"description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
|
||||
"placeholders": {
|
||||
"recommended": {
|
||||
@ -2505,7 +2505,7 @@
|
||||
}
|
||||
},
|
||||
"passphraseNumWordsRecommendationHint": {
|
||||
"message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.",
|
||||
"message": " 強力なパスフレーズを生成するには、 $RECOMMENDED$ 単語以上を使用してください。",
|
||||
"description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
|
||||
"placeholders": {
|
||||
"recommended": {
|
||||
@ -2558,11 +2558,11 @@
|
||||
"message": "外部転送サービスを使用してメールエイリアスを生成します。"
|
||||
},
|
||||
"forwarderDomainName": {
|
||||
"message": "Email domain",
|
||||
"message": "メールアドレスのドメイン",
|
||||
"description": "Labels the domain name email forwarder service option"
|
||||
},
|
||||
"forwarderDomainNameHint": {
|
||||
"message": "Choose a domain that is supported by the selected service",
|
||||
"message": "選択したサービスでサポートされているドメインを選択してください",
|
||||
"description": "Guidance provided for email forwarding services that support multiple email domains."
|
||||
},
|
||||
"forwarderError": {
|
||||
@ -2726,13 +2726,13 @@
|
||||
"message": "デバイスに通知を送信しました。"
|
||||
},
|
||||
"aNotificationWasSentToYourDevice": {
|
||||
"message": "A notification was sent to your device"
|
||||
"message": "お使いのデバイスに通知が送信されました"
|
||||
},
|
||||
"makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
|
||||
"message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
|
||||
"message": "アカウントがロック解除されていることと、フィンガープリントフレーズが他の端末と一致していることを確認してください"
|
||||
},
|
||||
"needAnotherOptionV1": {
|
||||
"message": "Need another option?"
|
||||
"message": "別の選択肢が必要ですか?"
|
||||
},
|
||||
"fingerprintMatchInfo": {
|
||||
"message": "保管庫がロックされていることと、パスフレーズが他のデバイスと一致していることを確認してください。"
|
||||
@ -2741,13 +2741,13 @@
|
||||
"message": "パスフレーズ"
|
||||
},
|
||||
"youWillBeNotifiedOnceTheRequestIsApproved": {
|
||||
"message": "You will be notified once the request is approved"
|
||||
"message": "リクエストが承認されると通知されます"
|
||||
},
|
||||
"needAnotherOption": {
|
||||
"message": "Bitwarden アプリの設定でデバイスでログインする必要があります。別のオプションが必要ですか?"
|
||||
},
|
||||
"viewAllLogInOptions": {
|
||||
"message": "View all log in options"
|
||||
"message": "すべてのログインオプションを表示"
|
||||
},
|
||||
"viewAllLoginOptions": {
|
||||
"message": "すべてのログインオプションを表示"
|
||||
@ -2869,7 +2869,7 @@
|
||||
"message": "このパスワードの既知のデータ流出を確認"
|
||||
},
|
||||
"loggedInExclamation": {
|
||||
"message": "Logged in!"
|
||||
"message": "ログインしました!"
|
||||
},
|
||||
"important": {
|
||||
"message": "重要"
|
||||
@ -2902,16 +2902,16 @@
|
||||
"message": "設定の更新を推奨"
|
||||
},
|
||||
"rememberThisDeviceToMakeFutureLoginsSeamless": {
|
||||
"message": "Remember this device to make future logins seamless"
|
||||
"message": "このデバイスを記憶して今後のログインをシームレスにする"
|
||||
},
|
||||
"deviceApprovalRequired": {
|
||||
"message": "デバイスの承認が必要です。以下から承認オプションを選択してください:"
|
||||
},
|
||||
"deviceApprovalRequiredV2": {
|
||||
"message": "Device approval required"
|
||||
"message": "デバイスの承認が必要です"
|
||||
},
|
||||
"selectAnApprovalOptionBelow": {
|
||||
"message": "Select an approval option below"
|
||||
"message": "以下の承認オプションを選択してください"
|
||||
},
|
||||
"rememberThisDevice": {
|
||||
"message": "このデバイスを記憶する"
|
||||
@ -2966,7 +2966,7 @@
|
||||
"message": "ユーザーのメールアドレスがありません"
|
||||
},
|
||||
"activeUserEmailNotFoundLoggingYouOut": {
|
||||
"message": "Active user email not found. Logging you out."
|
||||
"message": "アクティブなユーザーメールアドレスが見つかりません。ログアウトします。"
|
||||
},
|
||||
"deviceTrusted": {
|
||||
"message": "信頼されたデバイス"
|
||||
@ -3363,55 +3363,55 @@
|
||||
"message": "SSO ログインのための空きポートが見つかりませんでした。"
|
||||
},
|
||||
"authorize": {
|
||||
"message": "Authorize"
|
||||
"message": "認可"
|
||||
},
|
||||
"deny": {
|
||||
"message": "Deny"
|
||||
"message": "拒否"
|
||||
},
|
||||
"sshkeyApprovalTitle": {
|
||||
"message": "Confirm SSH key usage"
|
||||
"message": "SSH 鍵の使用を確認します"
|
||||
},
|
||||
"sshkeyApprovalMessageInfix": {
|
||||
"message": "is requesting access to"
|
||||
"message": "がアクセスを要求しています: "
|
||||
},
|
||||
"unknownApplication": {
|
||||
"message": "An application"
|
||||
"message": "アプリ"
|
||||
},
|
||||
"sshKeyPasswordUnsupported": {
|
||||
"message": "Importing password protected SSH keys is not yet supported"
|
||||
"message": "パスワードで保護された SSH キーのインポートはまだサポートされていません"
|
||||
},
|
||||
"invalidSshKey": {
|
||||
"message": "The SSH key is invalid"
|
||||
"message": "SSH キーが無効です"
|
||||
},
|
||||
"sshKeyTypeUnsupported": {
|
||||
"message": "The SSH key type is not supported"
|
||||
"message": "サポートされていない種類の SSH キーです"
|
||||
},
|
||||
"importSshKeyFromClipboard": {
|
||||
"message": "Import key from clipboard"
|
||||
"message": "クリップボードからキーをインポート"
|
||||
},
|
||||
"sshKeyPasted": {
|
||||
"message": "SSH key imported successfully"
|
||||
"message": "SSH キーのインポートに成功しました"
|
||||
},
|
||||
"fileSavedToDevice": {
|
||||
"message": "ファイルをデバイスに保存しました。デバイスのダウンロードで管理できます。"
|
||||
},
|
||||
"importantNotice": {
|
||||
"message": "Important notice"
|
||||
"message": "重要なお知らせ"
|
||||
},
|
||||
"setupTwoStepLogin": {
|
||||
"message": "Set up two-step login"
|
||||
"message": "2段階認証によるログインを設定する"
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage1": {
|
||||
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025."
|
||||
"message": "Bitwarden は2025年2月以降、新しいデバイスからのログイン時にアカウントのメールアドレスに確認コードを送信します。"
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage2": {
|
||||
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access."
|
||||
"message": "代わりに2段階認証によるログインでアカウントを保護するか、メールアドレスをあなたがアクセスできるものに変更できます。"
|
||||
},
|
||||
"remindMeLater": {
|
||||
"message": "Remind me later"
|
||||
"message": "後で再通知"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneFormContent": {
|
||||
"message": "Do you have reliable access to your email, $EMAIL$?",
|
||||
"message": "新しいメールアドレス $EMAIL$ はあなたが管理しているものですか?",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
@ -3420,15 +3420,15 @@
|
||||
}
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessNo": {
|
||||
"message": "No, I do not"
|
||||
"message": "いいえ、違います"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessYes": {
|
||||
"message": "Yes, I can reliably access my email"
|
||||
"message": "はい、メールアドレスには私が確実にアクセスできます"
|
||||
},
|
||||
"turnOnTwoStepLogin": {
|
||||
"message": "Turn on two-step login"
|
||||
"message": "2段階認証によるログインを有効にする"
|
||||
},
|
||||
"changeAcctEmail": {
|
||||
"message": "Change account email"
|
||||
"message": "アカウントのメールアドレスを変更する"
|
||||
}
|
||||
}
|
||||
|
@ -945,7 +945,7 @@
|
||||
"message": "Pictogrammenserver-URL"
|
||||
},
|
||||
"environmentSaved": {
|
||||
"message": "De omgevings-URL's zijn opgeslagen."
|
||||
"message": "Omgevings-URL's opgeslagen"
|
||||
},
|
||||
"ok": {
|
||||
"message": "Ok"
|
||||
@ -1002,7 +1002,7 @@
|
||||
"message": "Nieuwe map toevoegen"
|
||||
},
|
||||
"view": {
|
||||
"message": "Beeld"
|
||||
"message": "Weergeven"
|
||||
},
|
||||
"account": {
|
||||
"message": "Account"
|
||||
@ -1268,7 +1268,7 @@
|
||||
"description": "Copy to clipboard"
|
||||
},
|
||||
"checkForUpdates": {
|
||||
"message": "Controleren op updates"
|
||||
"message": "Controleren op updates…"
|
||||
},
|
||||
"version": {
|
||||
"message": "Versie $VERSION_NUM$",
|
||||
@ -3026,7 +3026,7 @@
|
||||
}
|
||||
},
|
||||
"multipleInputEmails": {
|
||||
"message": "Een of meer e-mailadressen zijn ongeldig"
|
||||
"message": "Eén of meer e-mailadressen zijn ongeldig"
|
||||
},
|
||||
"inputTrimValidator": {
|
||||
"message": "Invoer mag niet alleen witruimte bevatten.",
|
||||
@ -3051,7 +3051,7 @@
|
||||
"message": "-- Type om te filteren --"
|
||||
},
|
||||
"multiSelectLoading": {
|
||||
"message": "Opties ophalen..."
|
||||
"message": "Opties ophalen…"
|
||||
},
|
||||
"multiSelectNotFound": {
|
||||
"message": "Geen items gevonden"
|
||||
@ -3243,7 +3243,7 @@
|
||||
"message": "LastPass Email"
|
||||
},
|
||||
"importingYourAccount": {
|
||||
"message": "Account impoteren..."
|
||||
"message": "Account impoteren…"
|
||||
},
|
||||
"lastPassMFARequired": {
|
||||
"message": "LastPass multifactor-authenticatie vereist"
|
||||
@ -3396,7 +3396,7 @@
|
||||
"message": "Bestand op apparaat opgeslagen. Beheer vanaf de downloads op je apparaat."
|
||||
},
|
||||
"importantNotice": {
|
||||
"message": "Belangrijke mededeling"
|
||||
"message": "Belangrijke melding"
|
||||
},
|
||||
"setupTwoStepLogin": {
|
||||
"message": "Tweestapsaanmelding instellen"
|
||||
|
@ -3399,7 +3399,7 @@
|
||||
"message": "Dôležité upozornenie"
|
||||
},
|
||||
"setupTwoStepLogin": {
|
||||
"message": "Nastavenie dvojstupňového prihlásenia"
|
||||
"message": "Nastaviť dvojstupňové prihlásenie"
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage1": {
|
||||
"message": "Bitwarden vám od februára 2025 pošle na e-mail vášho účtu kód na overenie prihlásenia z nových zariadení."
|
||||
|
@ -208,19 +208,19 @@
|
||||
"message": "Генерисан је нови SSH кључ"
|
||||
},
|
||||
"sshKeyWrongPassword": {
|
||||
"message": "The password you entered is incorrect."
|
||||
"message": "Лозинка коју сте унели није тачна."
|
||||
},
|
||||
"importSshKey": {
|
||||
"message": "Import"
|
||||
"message": "Увоз"
|
||||
},
|
||||
"confirmSshKeyPassword": {
|
||||
"message": "Confirm password"
|
||||
"message": "Потврда лозинке"
|
||||
},
|
||||
"enterSshKeyPasswordDesc": {
|
||||
"message": "Enter the password for the SSH key."
|
||||
"message": "Унети лозинку за SSH кључ."
|
||||
},
|
||||
"enterSshKeyPassword": {
|
||||
"message": "Enter password"
|
||||
"message": "Унесите лозинку"
|
||||
},
|
||||
"sshAgentUnlockRequired": {
|
||||
"message": "Откључајте свој сеф да бисте одобрили захтев за SSH кључ."
|
||||
@ -920,10 +920,10 @@
|
||||
"message": "УРЛ Сервера"
|
||||
},
|
||||
"authenticationTimeout": {
|
||||
"message": "Authentication timeout"
|
||||
"message": "Истекло је време аутентификације"
|
||||
},
|
||||
"authenticationSessionTimedOut": {
|
||||
"message": "The authentication session timed out. Please restart the login process."
|
||||
"message": "Истекло је време сесије за аутентификацију. Молим вас покрените процес пријаве поново."
|
||||
},
|
||||
"selfHostBaseUrl": {
|
||||
"message": "УРЛ сервера који се самостално хостује",
|
||||
@ -1393,13 +1393,13 @@
|
||||
"message": "Историја Лозинке"
|
||||
},
|
||||
"generatorHistory": {
|
||||
"message": "Generator history"
|
||||
"message": "Генератор историје"
|
||||
},
|
||||
"clearGeneratorHistoryTitle": {
|
||||
"message": "Clear generator history"
|
||||
"message": "Испразнити генератор историје"
|
||||
},
|
||||
"cleargGeneratorHistoryDescription": {
|
||||
"message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?"
|
||||
"message": "Ако наставите, сви уноси ће бити трајно избрисани из генератора историје. Да ли сте сигурни да желите да наставите?"
|
||||
},
|
||||
"clear": {
|
||||
"message": "Очисти",
|
||||
@ -1409,13 +1409,13 @@
|
||||
"message": "Нема лозинки за приказивање."
|
||||
},
|
||||
"clearHistory": {
|
||||
"message": "Clear history"
|
||||
"message": "Обриши историју"
|
||||
},
|
||||
"nothingToShow": {
|
||||
"message": "Nothing to show"
|
||||
"message": "Ништа за приказ"
|
||||
},
|
||||
"nothingGeneratedRecently": {
|
||||
"message": "You haven't generated anything recently"
|
||||
"message": "Недавно нисте ништа генерисали"
|
||||
},
|
||||
"undo": {
|
||||
"message": "Опозови"
|
||||
@ -2481,7 +2481,7 @@
|
||||
"message": "Генеришите имејл"
|
||||
},
|
||||
"spinboxBoundariesHint": {
|
||||
"message": "Value must be between $MIN$ and $MAX$.",
|
||||
"message": "Вредност мора бити између $MIN$ и $MAX$.",
|
||||
"description": "Explains spin box minimum and maximum values to the user",
|
||||
"placeholders": {
|
||||
"min": {
|
||||
@ -2495,7 +2495,7 @@
|
||||
}
|
||||
},
|
||||
"passwordLengthRecommendationHint": {
|
||||
"message": " Use $RECOMMENDED$ characters or more to generate a strong password.",
|
||||
"message": " Употребити $RECOMMENDED$ знакова или више да бисте генерисали јаку лозинку.",
|
||||
"description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
|
||||
"placeholders": {
|
||||
"recommended": {
|
||||
@ -2505,7 +2505,7 @@
|
||||
}
|
||||
},
|
||||
"passphraseNumWordsRecommendationHint": {
|
||||
"message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.",
|
||||
"message": " Употребити $RECOMMENDED$ речи или више да бисте генерисали јаку приступну фразу.",
|
||||
"description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
|
||||
"placeholders": {
|
||||
"recommended": {
|
||||
@ -2726,13 +2726,13 @@
|
||||
"message": "Обавештење је послато на ваш уређај."
|
||||
},
|
||||
"aNotificationWasSentToYourDevice": {
|
||||
"message": "A notification was sent to your device"
|
||||
"message": "Обавештење је послато на ваш уређај"
|
||||
},
|
||||
"makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
|
||||
"message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
|
||||
"message": "Уверите се да је ваш налог откључан и да се фраза отиска подудара на другом уређају"
|
||||
},
|
||||
"needAnotherOptionV1": {
|
||||
"message": "Need another option?"
|
||||
"message": "Треба Вам друга опција?"
|
||||
},
|
||||
"fingerprintMatchInfo": {
|
||||
"message": "Уверите се да је ваш сеф откључан и да се фраза отиска прста подудара на другом уређају."
|
||||
@ -2741,13 +2741,13 @@
|
||||
"message": "Сигурносна фраза сефа"
|
||||
},
|
||||
"youWillBeNotifiedOnceTheRequestIsApproved": {
|
||||
"message": "You will be notified once the request is approved"
|
||||
"message": "Бићете обавештени када захтев буде одобрен"
|
||||
},
|
||||
"needAnotherOption": {
|
||||
"message": "Пријава помоћу уређаја мора бити подешена у подешавањима Bitwarden апликације. Потребна је друга опција?"
|
||||
},
|
||||
"viewAllLogInOptions": {
|
||||
"message": "View all log in options"
|
||||
"message": "Погледајте сав извештај у опције"
|
||||
},
|
||||
"viewAllLoginOptions": {
|
||||
"message": "Погредајте све опције пријављивања"
|
||||
@ -2869,7 +2869,7 @@
|
||||
"message": "Проверите познате упада података за ову лозинку"
|
||||
},
|
||||
"loggedInExclamation": {
|
||||
"message": "Logged in!"
|
||||
"message": "Пријављено!"
|
||||
},
|
||||
"important": {
|
||||
"message": "Важно:"
|
||||
@ -2902,16 +2902,16 @@
|
||||
"message": "Препоручено ажурирање поставки"
|
||||
},
|
||||
"rememberThisDeviceToMakeFutureLoginsSeamless": {
|
||||
"message": "Remember this device to make future logins seamless"
|
||||
"message": "Запамтити овај уређај да би будуће пријаве биле беспрекорне"
|
||||
},
|
||||
"deviceApprovalRequired": {
|
||||
"message": "Потребно је одобрење уређаја. Изаберите опцију одобрења испод:"
|
||||
},
|
||||
"deviceApprovalRequiredV2": {
|
||||
"message": "Device approval required"
|
||||
"message": "Потребно је одобрење уређаја"
|
||||
},
|
||||
"selectAnApprovalOptionBelow": {
|
||||
"message": "Select an approval option below"
|
||||
"message": "Изаберите опцију одобрења у наставку"
|
||||
},
|
||||
"rememberThisDevice": {
|
||||
"message": "Запамти овај уређај"
|
||||
@ -2966,7 +2966,7 @@
|
||||
"message": "Недостаје имејл корисника"
|
||||
},
|
||||
"activeUserEmailNotFoundLoggingYouOut": {
|
||||
"message": "Active user email not found. Logging you out."
|
||||
"message": "Имејл активног корисника није пронађен. Одјављивање."
|
||||
},
|
||||
"deviceTrusted": {
|
||||
"message": "Уређај поуздан"
|
||||
|
@ -3363,10 +3363,10 @@
|
||||
"message": "SSO girişi için açık port bulunamadı."
|
||||
},
|
||||
"authorize": {
|
||||
"message": "Authorize"
|
||||
"message": "Yetkilendir"
|
||||
},
|
||||
"deny": {
|
||||
"message": "Deny"
|
||||
"message": "Reddet"
|
||||
},
|
||||
"sshkeyApprovalTitle": {
|
||||
"message": "Confirm SSH key usage"
|
||||
|
@ -742,7 +742,7 @@
|
||||
"message": "必须填写确认主密码。"
|
||||
},
|
||||
"masterPasswordMinlength": {
|
||||
"message": "主密码必须至少 $VALUE$ 个字符长度。",
|
||||
"message": "主密码长度必须至少为 $VALUE$ 个字符。",
|
||||
"description": "The Master Password must be at least a specific number of characters long.",
|
||||
"placeholders": {
|
||||
"value": {
|
||||
@ -1044,7 +1044,7 @@
|
||||
"message": "前往网页 App 吗?"
|
||||
},
|
||||
"changeMasterPasswordOnWebConfirmation": {
|
||||
"message": "您可以在 Bitwarden 网页应用上更改您的主密码。"
|
||||
"message": "您可以在 Bitwarden 网页 App 上更改您的主密码。"
|
||||
},
|
||||
"fingerprintPhrase": {
|
||||
"message": "指纹短语",
|
||||
@ -1058,7 +1058,7 @@
|
||||
"message": "转到网页版密码库"
|
||||
},
|
||||
"getMobileApp": {
|
||||
"message": "获取移动应用程序"
|
||||
"message": "获取移动 App"
|
||||
},
|
||||
"getBrowserExtension": {
|
||||
"message": "获取浏览器扩展"
|
||||
@ -1247,13 +1247,13 @@
|
||||
"message": "语言"
|
||||
},
|
||||
"languageDesc": {
|
||||
"message": "更改应用程序所使用的语言。重新启动后生效。"
|
||||
"message": "更改应用程序所使用的语言。重启后生效。"
|
||||
},
|
||||
"theme": {
|
||||
"message": "主题"
|
||||
},
|
||||
"themeDesc": {
|
||||
"message": "更改本应用程序的颜色主题。"
|
||||
"message": "更改应用程序的颜色主题。"
|
||||
},
|
||||
"dark": {
|
||||
"message": "深色",
|
||||
@ -1665,7 +1665,7 @@
|
||||
"message": "确认密码库导出"
|
||||
},
|
||||
"exportWarningDesc": {
|
||||
"message": "导出的密码库数据包含未加密格式。您不应该通过不安全的渠道(例如电子邮件)来存储或发送导出的文件。用完后请立即将其删除。"
|
||||
"message": "此导出包含未加密格式的密码库数据。您不应该通过不安全的渠道(例如电子邮件)来存储或发送此导出文件。使用完后请立即将其删除。"
|
||||
},
|
||||
"encExportKeyWarningDesc": {
|
||||
"message": "此导出将使用您账户的加密密钥来加密您的数据。如果您曾经轮换过账户的加密密钥,您应将其重新导出,否则您将无法解密导出的文件。"
|
||||
@ -1711,7 +1711,7 @@
|
||||
"message": "使用 PIN 码解锁"
|
||||
},
|
||||
"setYourPinCode": {
|
||||
"message": "设定您用来解锁 Bitwarden 的 PIN 码。您的 PIN 设置将在您完全注销本应用程序时被重置。"
|
||||
"message": "设置用于解锁 Bitwarden 的 PIN 码。您的 PIN 设置将在您完全注销应用程序时被重置。"
|
||||
},
|
||||
"pinRequired": {
|
||||
"message": "需要 PIN 码。"
|
||||
@ -1753,7 +1753,7 @@
|
||||
"message": "应用程序启动时要求使用触控 ID"
|
||||
},
|
||||
"requirePasswordOnStart": {
|
||||
"message": "应用程序启动时要求输入密码或 PIN 码"
|
||||
"message": "App 启动时要求输入密码或 PIN 码"
|
||||
},
|
||||
"recommendedForSecurity": {
|
||||
"message": "安全起见,推荐设置。"
|
||||
@ -2402,7 +2402,7 @@
|
||||
"message": "偏好设置"
|
||||
},
|
||||
"appPreferences": {
|
||||
"message": "应用设置(所有账户)"
|
||||
"message": "应用程序设置(所有账户)"
|
||||
},
|
||||
"accountSwitcherLimitReached": {
|
||||
"message": "已达到账户上限。请注销一个账户后再添加其他账户。"
|
||||
@ -3369,7 +3369,7 @@
|
||||
"message": "拒绝"
|
||||
},
|
||||
"sshkeyApprovalTitle": {
|
||||
"message": "确认 SSH 密钥的使用方式"
|
||||
"message": "确认 SSH 密钥的使用"
|
||||
},
|
||||
"sshkeyApprovalMessageInfix": {
|
||||
"message": "正在请求访问"
|
||||
@ -3402,7 +3402,7 @@
|
||||
"message": "设置两步登录"
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage1": {
|
||||
"message": "从 2025 年 02 月开始,Bitwarden 将向您的账户电子邮箱发送一个代码,以验证来自新设备的登录。"
|
||||
"message": "从 2025 年 02 月起,当有来自新设备的登录时,Bitwarden 将向您的账户电子邮箱发送验证码。"
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage2": {
|
||||
"message": "您可以设置两步登录作为保护账户的替代方法,或将您的电子邮箱更改为您可以访问的电子邮箱。"
|
||||
@ -3411,7 +3411,7 @@
|
||||
"message": "稍后提醒我"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneFormContent": {
|
||||
"message": "您能可靠地访问您的电子邮箱 $EMAIL$ 吗?",
|
||||
"message": "您能可正常访问您的电子邮箱 $EMAIL$ 吗?",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
@ -3423,7 +3423,7 @@
|
||||
"message": "不,我不能"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessYes": {
|
||||
"message": "是的,我可以可靠地访问我的电子邮箱"
|
||||
"message": "是的,我可以正常访问我的电子邮箱"
|
||||
},
|
||||
"turnOnTwoStepLogin": {
|
||||
"message": "开启两步登录"
|
||||
|
@ -0,0 +1,38 @@
|
||||
<bit-dialog #dialog dialogSize="large" background="alt">
|
||||
<span bitDialogTitle>{{ "generator" | i18n }}</span>
|
||||
<ng-container bitDialogContent>
|
||||
<vault-cipher-form-generator
|
||||
[type]="data.type"
|
||||
(valueGenerated)="onCredentialGenerated($event)"
|
||||
/>
|
||||
<bit-item>
|
||||
<button
|
||||
type="button"
|
||||
bitLink
|
||||
linkType="primary"
|
||||
bit-item-content
|
||||
aria-haspopup="true"
|
||||
(click)="openHistoryDialog()"
|
||||
>
|
||||
{{ "generatorHistory" | i18n }}
|
||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||
</button>
|
||||
</bit-item>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="button"
|
||||
class="primary"
|
||||
(click)="applyCredentials()"
|
||||
appA11yTitle="{{ 'select' | i18n }}"
|
||||
bitDialogClose
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-fw bwi-check" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button type="button" data-dismiss="modal" (click)="clearCredentials()" bitDialogClose>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
@ -0,0 +1,69 @@
|
||||
import { DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import {
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
DialogService,
|
||||
ItemModule,
|
||||
LinkModule,
|
||||
} from "@bitwarden/components";
|
||||
import {
|
||||
CredentialGeneratorHistoryDialogComponent,
|
||||
GeneratorModule,
|
||||
} from "@bitwarden/generator-components";
|
||||
import { CipherFormGeneratorComponent } from "@bitwarden/vault";
|
||||
|
||||
type CredentialGeneratorParams = {
|
||||
onCredentialGenerated: (value?: string) => void;
|
||||
type: "password" | "username";
|
||||
};
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "credential-generator-dialog",
|
||||
templateUrl: "credential-generator-dialog.component.html",
|
||||
imports: [
|
||||
CipherFormGeneratorComponent,
|
||||
CommonModule,
|
||||
DialogModule,
|
||||
ButtonModule,
|
||||
JslibModule,
|
||||
GeneratorModule,
|
||||
ItemModule,
|
||||
LinkModule,
|
||||
],
|
||||
})
|
||||
export class CredentialGeneratorDialogComponent {
|
||||
credentialValue?: string;
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected data: CredentialGeneratorParams,
|
||||
private dialogService: DialogService,
|
||||
) {}
|
||||
|
||||
applyCredentials = () => {
|
||||
this.data.onCredentialGenerated(this.credentialValue);
|
||||
};
|
||||
|
||||
clearCredentials = () => {
|
||||
this.data.onCredentialGenerated();
|
||||
};
|
||||
|
||||
onCredentialGenerated = (value: string) => {
|
||||
this.credentialValue = value;
|
||||
};
|
||||
|
||||
openHistoryDialog = () => {
|
||||
// open history dialog
|
||||
this.dialogService.open(CredentialGeneratorHistoryDialogComponent);
|
||||
};
|
||||
|
||||
static open = (dialogService: DialogService, data: CredentialGeneratorParams) => {
|
||||
dialogService.open(CredentialGeneratorDialogComponent, {
|
||||
data,
|
||||
});
|
||||
};
|
||||
}
|
@ -20,7 +20,9 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@ -40,6 +42,7 @@ import { invokeMenu, RendererMenuItem } from "../../../utils";
|
||||
import { AddEditComponent } from "./add-edit.component";
|
||||
import { AttachmentsComponent } from "./attachments.component";
|
||||
import { CollectionsComponent } from "./collections.component";
|
||||
import { CredentialGeneratorDialogComponent } from "./credential-generator-dialog.component";
|
||||
import { FolderAddEditComponent } from "./folder-add-edit.component";
|
||||
import { PasswordHistoryComponent } from "./password-history.component";
|
||||
import { ShareComponent } from "./share.component";
|
||||
@ -107,6 +110,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
private apiService: ApiService,
|
||||
private dialogService: DialogService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@ -622,11 +626,29 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
async openGenerator(comingFromAddEdit: boolean, passwordType = true) {
|
||||
// FIXME: Will need to be extended to use the cipher-form-generator component introduced with https://github.com/bitwarden/clients/pull/11350
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
const isGeneratorSwapEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.GeneratorToolsModernization,
|
||||
);
|
||||
|
||||
if (isGeneratorSwapEnabled) {
|
||||
CredentialGeneratorDialogComponent.open(this.dialogService, {
|
||||
onCredentialGenerated: (value?: string) => {
|
||||
if (this.addEditComponent != null) {
|
||||
this.addEditComponent.markPasswordAsDirty();
|
||||
if (passwordType) {
|
||||
this.addEditComponent.cipher.login.password = value ?? "";
|
||||
} else {
|
||||
this.addEditComponent.cipher.login.username = value ?? "";
|
||||
}
|
||||
}
|
||||
},
|
||||
type: passwordType ? "password" : "username",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Legacy code below, remove once the new generator is fully implemented
|
||||
// https://bitwarden.atlassian.net/browse/PM-7121
|
||||
const cipher = this.addEditComponent?.cipher;
|
||||
const loginType = cipher != null && cipher.type === CipherType.Login && cipher.login != null;
|
||||
|
||||
|
@ -9,9 +9,11 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
|
||||
import { IntegrationType } from "@bitwarden/common/enums";
|
||||
|
||||
import { HeaderModule } from "../../../layouts/header/header.module";
|
||||
import { FilterIntegrationsPipe, IntegrationGridComponent, Integration } from "../../../shared/";
|
||||
import { SharedModule } from "../../../shared/shared.module";
|
||||
import { SharedOrganizationModule } from "../shared";
|
||||
import { IntegrationGridComponent } from "../shared/components/integrations/integration-grid/integration-grid.component";
|
||||
import { FilterIntegrationsPipe } from "../shared/components/integrations/integrations.pipe";
|
||||
import { Integration } from "../shared/components/integrations/models";
|
||||
|
||||
@Component({
|
||||
selector: "ac-integrations",
|
||||
|
@ -131,7 +131,9 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe
|
||||
|
||||
protected getUserName(r: EventResponse, userId: string) {
|
||||
if (r.installationId != null) {
|
||||
return `Installation: ${r.installationId}`;
|
||||
return {
|
||||
name: `Installation: ${r.installationId}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (userId != null) {
|
||||
|
@ -0,0 +1,4 @@
|
||||
export * from "./integrations.pipe";
|
||||
export * from "./integration-card/integration-card.component";
|
||||
export * from "./integration-grid/integration-grid.component";
|
||||
export * from "./models";
|
@ -15,7 +15,7 @@ import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-t
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
|
||||
import { SharedModule } from "../../../shared.module";
|
||||
import { SharedModule } from "../../../../../../shared/shared.module";
|
||||
|
||||
@Component({
|
||||
selector: "app-integration-card",
|
@ -1,13 +1,12 @@
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
import { importProvidersFrom } from "@angular/core";
|
||||
import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
import { I18nMockService } from "@bitwarden/components";
|
||||
|
||||
import { SharedModule } from "../../../shared.module";
|
||||
import { PreloadedEnglishI18nModule } from "../../../../../../core/tests";
|
||||
|
||||
import { IntegrationCardComponent } from "./integration-card.component";
|
||||
|
||||
@ -17,15 +16,11 @@ export default {
|
||||
title: "Web/Integration Layout/Integration Card",
|
||||
component: IntegrationCardComponent,
|
||||
decorators: [
|
||||
applicationConfig({
|
||||
providers: [importProvidersFrom(PreloadedEnglishI18nModule)],
|
||||
}),
|
||||
moduleMetadata({
|
||||
imports: [SharedModule],
|
||||
providers: [
|
||||
{
|
||||
provide: I18nService,
|
||||
useFactory: () => {
|
||||
return new I18nMockService({});
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ThemeStateService,
|
||||
useClass: MockThemeService,
|
@ -4,7 +4,7 @@ import { Component, Input } from "@angular/core";
|
||||
|
||||
import { IntegrationType } from "@bitwarden/common/enums";
|
||||
|
||||
import { SharedModule } from "../../../shared.module";
|
||||
import { SharedModule } from "../../../../../../shared/shared.module";
|
||||
import { IntegrationCardComponent } from "../integration-card/integration-card.component";
|
||||
import { Integration } from "../models";
|
||||
|
@ -1,14 +1,13 @@
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
import { importProvidersFrom } from "@angular/core";
|
||||
import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { IntegrationType } from "@bitwarden/common/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
import { I18nMockService } from "@bitwarden/components";
|
||||
|
||||
import { SharedModule } from "../../../shared.module";
|
||||
import { PreloadedEnglishI18nModule } from "../../../../../../core/tests";
|
||||
import { IntegrationCardComponent } from "../integration-card/integration-card.component";
|
||||
import { IntegrationGridComponent } from "../integration-grid/integration-grid.component";
|
||||
|
||||
@ -18,18 +17,12 @@ export default {
|
||||
title: "Web/Integration Layout/Integration Grid",
|
||||
component: IntegrationGridComponent,
|
||||
decorators: [
|
||||
applicationConfig({
|
||||
providers: [importProvidersFrom(PreloadedEnglishI18nModule)],
|
||||
}),
|
||||
moduleMetadata({
|
||||
imports: [IntegrationCardComponent, SharedModule],
|
||||
imports: [IntegrationCardComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: I18nService,
|
||||
useFactory: () => {
|
||||
return new I18nMockService({
|
||||
integrationCardAriaLabel: "Go to integration",
|
||||
integrationCardTooltip: "Go to integration",
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ThemeStateService,
|
||||
useClass: MockThemeService,
|
@ -3,7 +3,7 @@ import { LayoutModule } from "@angular/cdk/layout";
|
||||
import { NgModule } from "@angular/core";
|
||||
import { FormsModule } from "@angular/forms";
|
||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { InfiniteScrollModule } from "ngx-infinite-scroll";
|
||||
import { InfiniteScrollDirective } from "ngx-infinite-scroll";
|
||||
|
||||
import { AppComponent } from "./app.component";
|
||||
import { CoreModule } from "./core";
|
||||
@ -23,7 +23,7 @@ import { WildcardRoutingModule } from "./wildcard-routing.module";
|
||||
BrowserAnimationsModule,
|
||||
FormsModule,
|
||||
CoreModule,
|
||||
InfiniteScrollModule,
|
||||
InfiniteScrollDirective,
|
||||
DragDropModule,
|
||||
LayoutModule,
|
||||
OssRoutingModule,
|
||||
|
@ -1,4 +0,0 @@
|
||||
export * from "./integrations/integration-card/integration-card.component";
|
||||
export * from "./integrations/integration-grid/integration-grid.component";
|
||||
export * from "./integrations/integrations.pipe";
|
||||
export * from "./integrations/models";
|
@ -1,3 +1,2 @@
|
||||
export * from "./shared.module";
|
||||
export * from "./loose-components.module";
|
||||
export * from "./components/index";
|
||||
|
@ -3,7 +3,7 @@ import { CommonModule, DatePipe } from "@angular/common";
|
||||
import { NgModule } from "@angular/core";
|
||||
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||
import { RouterModule } from "@angular/router";
|
||||
import { InfiniteScrollModule } from "ngx-infinite-scroll";
|
||||
import { InfiniteScrollDirective } from "ngx-infinite-scroll";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import {
|
||||
@ -49,7 +49,7 @@ import "./locales";
|
||||
DragDropModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
InfiniteScrollModule,
|
||||
InfiniteScrollDirective,
|
||||
RouterModule,
|
||||
JslibModule,
|
||||
|
||||
@ -86,7 +86,7 @@ import "./locales";
|
||||
DragDropModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
InfiniteScrollModule,
|
||||
InfiniteScrollDirective,
|
||||
RouterModule,
|
||||
JslibModule,
|
||||
|
||||
|
@ -9,9 +9,11 @@ import { map } from "rxjs/operators";
|
||||
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
@ -237,6 +239,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
|
||||
private premiumUpgradeService: PremiumUpgradePromptService,
|
||||
private cipherAuthorizationService: CipherAuthorizationService,
|
||||
private apiService: ApiService,
|
||||
private eventCollectionService: EventCollectionService,
|
||||
) {
|
||||
this.updateTitle();
|
||||
}
|
||||
@ -257,6 +260,13 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
|
||||
[this.params.activeCollectionId],
|
||||
this.params.isAdminConsoleAction,
|
||||
);
|
||||
|
||||
await this.eventCollectionService.collect(
|
||||
EventType.Cipher_ClientViewed,
|
||||
this.cipher.id,
|
||||
false,
|
||||
this.cipher.organizationId,
|
||||
);
|
||||
}
|
||||
|
||||
this.performingInitialLoad = false;
|
||||
|
@ -9271,6 +9271,18 @@
|
||||
"updatedTaxInformation": {
|
||||
"message": "Updated tax information"
|
||||
},
|
||||
"billingInvalidTaxIdError": {
|
||||
"message": "Invalid tax ID, if you believe this is an error please contact support."
|
||||
},
|
||||
"billingTaxIdTypeInferenceError": {
|
||||
"message": "We were unable to validate your tax ID, if you believe this is an error please contact support."
|
||||
},
|
||||
"billingPreviewInvalidTaxIdError": {
|
||||
"message": "Invalid tax ID, if you believe this is an error please contact support."
|
||||
},
|
||||
"billingPreviewInvoiceError": {
|
||||
"message": "An error occurred while previewing the invoice. Please try again later."
|
||||
},
|
||||
"unverified": {
|
||||
"message": "Unverified"
|
||||
},
|
||||
@ -10007,5 +10019,48 @@
|
||||
},
|
||||
"organizationNameMaxLength": {
|
||||
"message": "Organization name cannot exceed 50 characters."
|
||||
},
|
||||
"resellerRenewalWarning": {
|
||||
"message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.",
|
||||
"placeholders": {
|
||||
"reseller": {
|
||||
"content": "$1",
|
||||
"example": "Reseller Name"
|
||||
},
|
||||
"renewal_date": {
|
||||
"content": "$2",
|
||||
"example": "01/01/2024"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resellerOpenInvoiceWarning": {
|
||||
"message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.",
|
||||
"placeholders": {
|
||||
"reseller": {
|
||||
"content": "$1",
|
||||
"example": "Reseller Name"
|
||||
},
|
||||
"issued_date": {
|
||||
"content": "$2",
|
||||
"example": "01/01/2024"
|
||||
},
|
||||
"due_date": {
|
||||
"content": "$3",
|
||||
"example": "01/15/2024"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resellerPastDueWarning": {
|
||||
"message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.",
|
||||
"placeholders": {
|
||||
"reseller": {
|
||||
"content": "$1",
|
||||
"example": "Reseller Name"
|
||||
},
|
||||
"grace_period_end": {
|
||||
"content": "$2",
|
||||
"example": "02/14/2024"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9271,6 +9271,18 @@
|
||||
"updatedTaxInformation": {
|
||||
"message": "Updated tax information"
|
||||
},
|
||||
"billingInvalidTaxIdError": {
|
||||
"message": "Invalid tax ID, if you believe this is an error please contact support."
|
||||
},
|
||||
"billingTaxIdTypeInferenceError": {
|
||||
"message": "We were unable to validate your tax ID, if you believe this is an error please contact support."
|
||||
},
|
||||
"billingPreviewInvalidTaxIdError": {
|
||||
"message": "Invalid tax ID, if you believe this is an error please contact support."
|
||||
},
|
||||
"billingPreviewInvoiceError": {
|
||||
"message": "An error occurred while previewing the invoice. Please try again later."
|
||||
},
|
||||
"unverified": {
|
||||
"message": "Unverified"
|
||||
},
|
||||
@ -10007,5 +10019,48 @@
|
||||
},
|
||||
"organizationNameMaxLength": {
|
||||
"message": "Organization name cannot exceed 50 characters."
|
||||
},
|
||||
"resellerRenewalWarning": {
|
||||
"message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.",
|
||||
"placeholders": {
|
||||
"reseller": {
|
||||
"content": "$1",
|
||||
"example": "Reseller Name"
|
||||
},
|
||||
"renewal_date": {
|
||||
"content": "$2",
|
||||
"example": "01/01/2024"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resellerOpenInvoiceWarning": {
|
||||
"message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.",
|
||||
"placeholders": {
|
||||
"reseller": {
|
||||
"content": "$1",
|
||||
"example": "Reseller Name"
|
||||
},
|
||||
"issued_date": {
|
||||
"content": "$2",
|
||||
"example": "01/01/2024"
|
||||
},
|
||||
"due_date": {
|
||||
"content": "$3",
|
||||
"example": "01/15/2024"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resellerPastDueWarning": {
|
||||
"message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.",
|
||||
"placeholders": {
|
||||
"reseller": {
|
||||
"content": "$1",
|
||||
"example": "Reseller Name"
|
||||
},
|
||||
"grace_period_end": {
|
||||
"content": "$2",
|
||||
"example": "02/14/2024"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9271,6 +9271,18 @@
|
||||
"updatedTaxInformation": {
|
||||
"message": "Güncəlllənən vergi məlumatı"
|
||||
},
|
||||
"billingInvalidTaxIdError": {
|
||||
"message": "Yararsız vergi kimliyi, bunun xəta olduğunu düşünürsünüzsə, dəstək komandası ilə əlaqə saxlayın."
|
||||
},
|
||||
"billingTaxIdTypeInferenceError": {
|
||||
"message": "Vergi kimliyi nömrənizi doğrulaya bilmədik, bunun xəta olduğunu düşünürsünüzsə, dəstək komandası ilə əlaqə saxlayın."
|
||||
},
|
||||
"billingPreviewInvalidTaxIdError": {
|
||||
"message": "Yararsız vergi kimliyi, bunun xəta olduğunu düşünürsünüzsə, dəstək komandası ilə əlaqə saxlayın."
|
||||
},
|
||||
"billingPreviewInvoiceError": {
|
||||
"message": "Faktura önizləməsi zamanı bir xəta baş verdi. Lütfən daha sonra yenidən sınayın."
|
||||
},
|
||||
"unverified": {
|
||||
"message": "Doğrulanmayıb"
|
||||
},
|
||||
@ -10007,5 +10019,48 @@
|
||||
},
|
||||
"organizationNameMaxLength": {
|
||||
"message": "Təşkilat adı 50 xarakterdən çox ola bilməz."
|
||||
},
|
||||
"resellerRenewalWarning": {
|
||||
"message": "Abunəliyiniz tezliklə yenilənəcək. Kəsintisiz xidməti təmin etmək və yeniləməni $RENEWAL_DATE$ tarixindən əvvəl təsdiqləmək üçün $RESELLER$ ilə əlaqə saxlayın.",
|
||||
"placeholders": {
|
||||
"reseller": {
|
||||
"content": "$1",
|
||||
"example": "Reseller Name"
|
||||
},
|
||||
"renewal_date": {
|
||||
"content": "$2",
|
||||
"example": "01/01/2024"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resellerOpenInvoiceWarning": {
|
||||
"message": "Abunəliyinizə aid faktura $ISSUED_DATE$ tarixində təqdim edildi. Kəsintisiz xidməti təmin etmək və yeniləməni $DUE_DATE$ tarixindən əvvəl təsdiqləmək üçün $RESELLER$ ilə əlaqə saxlayın.",
|
||||
"placeholders": {
|
||||
"reseller": {
|
||||
"content": "$1",
|
||||
"example": "Reseller Name"
|
||||
},
|
||||
"issued_date": {
|
||||
"content": "$2",
|
||||
"example": "01/01/2024"
|
||||
},
|
||||
"due_date": {
|
||||
"content": "$3",
|
||||
"example": "01/15/2024"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resellerPastDueWarning": {
|
||||
"message": "Abunəliyinizə aid faktura üzrə ödəniş edilmədi. Kəsintisiz xidməti təmin etmək və yeniləməni $GRACE_PERIOD_END$ tarixindən əvvəl təsdiqləmək üçün $RESELLER$ ilə əlaqə saxlayın.",
|
||||
"placeholders": {
|
||||
"reseller": {
|
||||
"content": "$1",
|
||||
"example": "Reseller Name"
|
||||
},
|
||||
"grace_period_end": {
|
||||
"content": "$2",
|
||||
"example": "02/14/2024"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9271,6 +9271,18 @@
|
||||
"updatedTaxInformation": {
|
||||
"message": "Updated tax information"
|
||||
},
|
||||
"billingInvalidTaxIdError": {
|
||||
"message": "Invalid tax ID, if you believe this is an error please contact support."
|
||||
},
|
||||
"billingTaxIdTypeInferenceError": {
|
||||
"message": "We were unable to validate your tax ID, if you believe this is an error please contact support."
|
||||
},
|
||||
"billingPreviewInvalidTaxIdError": {
|
||||
"message": "Invalid tax ID, if you believe this is an error please contact support."
|
||||
},
|
||||
"billingPreviewInvoiceError": {
|
||||
"message": "An error occurred while previewing the invoice. Please try again later."
|
||||
},
|
||||
"unverified": {
|
||||
"message": "Unverified"
|
||||
},
|
||||
@ -10007,5 +10019,48 @@
|
||||
},
|
||||
"organizationNameMaxLength": {
|
||||
"message": "Organization name cannot exceed 50 characters."
|
||||
},
|
||||
"resellerRenewalWarning": {
|
||||
"message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.",
|
||||
"placeholders": {
|
||||
"reseller": {
|
||||
"content": "$1",
|
||||
"example": "Reseller Name"
|
||||
},
|
||||
"renewal_date": {
|
||||
"content": "$2",
|
||||
"example": "01/01/2024"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resellerOpenInvoiceWarning": {
|
||||
"message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.",
|
||||
"placeholders": {
|
||||
"reseller": {
|
||||
"content": "$1",
|
||||
"example": "Reseller Name"
|
||||
},
|
||||
"issued_date": {
|
||||
"content": "$2",
|
||||
"example": "01/01/2024"
|
||||
},
|
||||
"due_date": {
|
||||
"content": "$3",
|
||||
"example": "01/15/2024"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resellerPastDueWarning": {
|
||||
"message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.",
|
||||
"placeholders": {
|
||||
"reseller": {
|
||||
"content": "$1",
|
||||
"example": "Reseller Name"
|
||||
},
|
||||
"grace_period_end": {
|
||||
"content": "$2",
|
||||
"example": "02/14/2024"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9271,6 +9271,18 @@
|
||||
"updatedTaxInformation": {
|
||||
"message": "Обновена данъчна информация"
|
||||
},
|
||||
"billingInvalidTaxIdError": {
|
||||
"message": "Неправилен данъчен идентификатор. Ако смятате, че това е грешка, свържете се с поддръжката."
|
||||
},
|
||||
"billingTaxIdTypeInferenceError": {
|
||||
"message": "Не успяхме да потвърдим Вашия данъчен идентификатор. Ако смятате, че това е грешка, свържете се с поддръжката."
|
||||
},
|
||||
"billingPreviewInvalidTaxIdError": {
|
||||
"message": "Неправилен данъчен идентификатор. Ако смятате, че това е грешка, свържете се с поддръжката."
|
||||
},
|
||||
"billingPreviewInvoiceError": {
|
||||
"message": "Възникна грешка при преглеждането на фактурата. Опитайте отново по-късно."
|
||||
},
|
||||
"unverified": {
|
||||
"message": "Непотвърден"
|
||||
},
|
||||
@ -10007,5 +10019,48 @@
|
||||
},
|
||||
"organizationNameMaxLength": {
|
||||
"message": "Името на организацията не може да бъде по-дълго от 50 знака."
|
||||
},
|
||||
"resellerRenewalWarning": {
|
||||
"message": "Вашият абонамент ще бъде подновен скоро. За да подсигурите, че услугата няма да има прекъсвания, свържете се с $RESELLER$ и потвърдете подновяването преди $RENEWAL_DATE$.",
|
||||
"placeholders": {
|
||||
"reseller": {
|
||||
"content": "$1",
|
||||
"example": "Reseller Name"
|
||||
},
|
||||
"renewal_date": {
|
||||
"content": "$2",
|
||||
"example": "01/01/2024"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resellerOpenInvoiceWarning": {
|
||||
"message": "Фактура за абонамента Ви беше издадена на $ISSUED_DATE$. За да подсигурите, че услугата няма да има прекъсвания, свържете се с $RESELLER$ и потвърдете подновяването преди $DUE_DATE$.",
|
||||
"placeholders": {
|
||||
"reseller": {
|
||||
"content": "$1",
|
||||
"example": "Reseller Name"
|
||||
},
|
||||
"issued_date": {
|
||||
"content": "$2",
|
||||
"example": "01/01/2024"
|
||||
},
|
||||
"due_date": {
|
||||
"content": "$3",
|
||||
"example": "01/15/2024"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resellerPastDueWarning": {
|
||||
"message": "Фактурата за абонамента Ви не е била платена. За да подсигурите, че услугата няма да има прекъсвания, свържете се с $RESELLER$ и потвърдете подновяването преди $GRACE_PERIOD_END$.",
|
||||
"placeholders": {
|
||||
"reseller": {
|
||||
"content": "$1",
|
||||
"example": "Reseller Name"
|
||||
},
|
||||
"grace_period_end": {
|
||||
"content": "$2",
|
||||
"example": "02/14/2024"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9271,6 +9271,18 @@
|
||||
"updatedTaxInformation": {
|
||||
"message": "Updated tax information"
|
||||
},
|
||||
"billingInvalidTaxIdError": {
|
||||
"message": "Invalid tax ID, if you believe this is an error please contact support."
|
||||
},
|
||||
"billingTaxIdTypeInferenceError": {
|
||||
"message": "We were unable to validate your tax ID, if you believe this is an error please contact support."
|
||||
},
|
||||
"billingPreviewInvalidTaxIdError": {
|
||||
"message": "Invalid tax ID, if you believe this is an error please contact support."
|
||||
},
|
||||
"billingPreviewInvoiceError": {
|
||||
"message": "An error occurred while previewing the invoice. Please try again later."
|
||||
},
|
||||
"unverified": {
|
||||
"message": "Unverified"
|
||||
},
|
||||
@ -10007,5 +10019,48 @@
|
||||
},
|
||||
"organizationNameMaxLength": {
|
||||
"message": "Organization name cannot exceed 50 characters."
|
||||
},
|
||||
"resellerRenewalWarning": {
|
||||
"message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.",
|
||||
"placeholders": {
|
||||
"reseller": {
|
||||
"content": "$1",
|
||||
"example": "Reseller Name"
|
||||
},
|
||||
"renewal_date": {
|
||||
"content": "$2",
|
||||
"example": "01/01/2024"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resellerOpenInvoiceWarning": {
|
||||
"message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.",
|
||||
"placeholders": {
|
||||
"reseller": {
|
||||
"content": "$1",
|
||||
"example": "Reseller Name"
|
||||
},
|
||||
"issued_date": {
|
||||
"content": "$2",
|
||||
"example": "01/01/2024"
|
||||
},
|
||||
"due_date": {
|
||||
"content": "$3",
|
||||
"example": "01/15/2024"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resellerPastDueWarning": {
|
||||
"message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.",
|
||||
"placeholders": {
|
||||
"reseller": {
|
||||
"content": "$1",
|
||||
"example": "Reseller Name"
|
||||
},
|
||||
"grace_period_end": {
|
||||
"content": "$2",
|
||||
"example": "02/14/2024"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9271,6 +9271,18 @@
|
||||
"updatedTaxInformation": {
|
||||
"message": "Updated tax information"
|
||||
},
|
||||
"billingInvalidTaxIdError": {
|
||||
"message": "Invalid tax ID, if you believe this is an error please contact support."
|
||||
},
|
||||
"billingTaxIdTypeInferenceError": {
|
||||
"message": "We were unable to validate your tax ID, if you believe this is an error please contact support."
|
||||
},
|
||||
"billingPreviewInvalidTaxIdError": {
|
||||
"message": "Invalid tax ID, if you believe this is an error please contact support."
|
||||
},
|
||||
"billingPreviewInvoiceError": {
|
||||
"message": "An error occurred while previewing the invoice. Please try again later."
|
||||
},
|
||||
"unverified": {
|
||||
"message": "Unverified"
|
||||
},
|
||||
@ -10007,5 +10019,48 @@
|
||||
},
|
||||
"organizationNameMaxLength": {
|
||||
"message": "Organization name cannot exceed 50 characters."
|
||||
},
|
||||
"resellerRenewalWarning": {
|
||||
"message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.",
|
||||
"placeholders": {
|
||||
"reseller": {
|
||||
"content": "$1",
|
||||
"example": "Reseller Name"
|
||||
},
|
||||
"renewal_date": {
|
||||
"content": "$2",
|
||||
"example": "01/01/2024"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resellerOpenInvoiceWarning": {
|
||||
"message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.",
|
||||
"placeholders": {
|
||||
"reseller": {
|
||||
"content": "$1",
|
||||
"example": "Reseller Name"
|
||||
},
|
||||
"issued_date": {
|
||||
"content": "$2",
|
||||
"example": "01/01/2024"
|
||||
},
|
||||
"due_date": {
|
||||
"content": "$3",
|
||||
"example": "01/15/2024"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resellerPastDueWarning": {
|
||||
"message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.",
|
||||
"placeholders": {
|
||||
"reseller": {
|
||||
"content": "$1",
|
||||
"example": "Reseller Name"
|
||||
},
|
||||
"grace_period_end": {
|
||||
"content": "$2",
|
||||
"example": "02/14/2024"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9271,6 +9271,18 @@
|
||||
"updatedTaxInformation": {
|
||||
"message": "Updated tax information"
|
||||
},
|
||||
"billingInvalidTaxIdError": {
|
||||
"message": "Invalid tax ID, if you believe this is an error please contact support."
|
||||
},
|
||||
"billingTaxIdTypeInferenceError": {
|
||||
"message": "We were unable to validate your tax ID, if you believe this is an error please contact support."
|
||||
},
|
||||
"billingPreviewInvalidTaxIdError": {
|
||||
"message": "Invalid tax ID, if you believe this is an error please contact support."
|
||||
},
|
||||
"billingPreviewInvoiceError": {
|
||||
"message": "An error occurred while previewing the invoice. Please try again later."
|
||||
},
|
||||
"unverified": {
|
||||
"message": "Unverified"
|
||||
},
|
||||
@ -10007,5 +10019,48 @@
|
||||
},
|
||||
"organizationNameMaxLength": {
|
||||
"message": "Organization name cannot exceed 50 characters."
|
||||
},
|
||||
"resellerRenewalWarning": {
|
||||
"message": "Your subscription will renew soon. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.",
|
||||
"placeholders": {
|
||||
"reseller": {
|
||||
"content": "$1",
|
||||
"example": "Reseller Name"
|
||||
},
|
||||
"renewal_date": {
|
||||
"content": "$2",
|
||||
"example": "01/01/2024"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resellerOpenInvoiceWarning": {
|
||||
"message": "An invoice for your subscription was issued on $ISSUED_DATE$. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $DUE_DATE$.",
|
||||
"placeholders": {
|
||||
"reseller": {
|
||||
"content": "$1",
|
||||
"example": "Reseller Name"
|
||||
},
|
||||
"issued_date": {
|
||||
"content": "$2",
|
||||
"example": "01/01/2024"
|
||||
},
|
||||
"due_date": {
|
||||
"content": "$3",
|
||||
"example": "01/15/2024"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resellerPastDueWarning": {
|
||||
"message": "The invoice for your subscription has not been paid. To insure uninterrupted service, contact $RESELLER$ to confirm your renewal before $GRACE_PERIOD_END$.",
|
||||
"placeholders": {
|
||||
"reseller": {
|
||||
"content": "$1",
|
||||
"example": "Reseller Name"
|
||||
},
|
||||
"grace_period_end": {
|
||||
"content": "$2",
|
||||
"example": "02/14/2024"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9271,6 +9271,18 @@
|
||||
"updatedTaxInformation": {
|
||||
"message": "Aktualizované daňové údaje"
|
||||
},
|
||||
"billingInvalidTaxIdError": {
|
||||
"message": "Neplatné DIČ. Pokud se domníváte, že se jedná o chybu, kontaktujte podporu."
|
||||
},
|
||||
"billingTaxIdTypeInferenceError": {
|
||||
"message": "Nebyli jsme schopni ověřit Vaše DIČ. Pokud se domníváte, že se jedná o chybu, kontaktujte podporu."
|
||||
},
|
||||
"billingPreviewInvalidTaxIdError": {
|
||||
"message": "Neplatné DIČ. Pokud se domníváte, že se jedná o chybu, kontaktujte podporu."
|
||||
},
|
||||
"billingPreviewInvoiceError": {
|
||||
"message": "Při náhledu faktury došlo k chybě. Opakujte akci později."
|
||||
},
|
||||
"unverified": {
|
||||
"message": "Neověřeno"
|
||||
},
|
||||
@ -10007,5 +10019,48 @@
|
||||
},
|
||||
"organizationNameMaxLength": {
|
||||
"message": "Název organizace nesmí přesáhnout 50 znaků."
|
||||
},
|
||||
"resellerRenewalWarning": {
|
||||
"message": "Vaše předplatné se brzy obnoví. Chcete-li zajistit nepřerušenou službu, kontaktujte $RESELLER$ pro potvrzení obnovení před $RENEWAL_DATE$.",
|
||||
"placeholders": {
|
||||
"reseller": {
|
||||
"content": "$1",
|
||||
"example": "Reseller Name"
|
||||
},
|
||||
"renewal_date": {
|
||||
"content": "$2",
|
||||
"example": "01/01/2024"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resellerOpenInvoiceWarning": {
|
||||
"message": "Faktura pro Vaše předplatné byla vystavena dne $ISSUED_DATE$. Chcete-li zajistit nepřerušovanou službu, kontaktujte $RESELLER$ pro potvrzení obnovení před $DUE_DATE$.",
|
||||
"placeholders": {
|
||||
"reseller": {
|
||||
"content": "$1",
|
||||
"example": "Reseller Name"
|
||||
},
|
||||
"issued_date": {
|
||||
"content": "$2",
|
||||
"example": "01/01/2024"
|
||||
},
|
||||
"due_date": {
|
||||
"content": "$3",
|
||||
"example": "01/15/2024"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resellerPastDueWarning": {
|
||||
"message": "Faktura za Vaše předplatné nebyla zaplacena. Chcete-li zajistit nepřerušovanou službu, kontaktujte $RESELLER$ pro potvrzení obnovení před $GRACE_PERIOD_END$.",
|
||||
"placeholders": {
|
||||
"reseller": {
|
||||
"content": "$1",
|
||||
"example": "Reseller Name"
|
||||
},
|
||||
"grace_period_end": {
|
||||
"content": "$2",
|
||||
"example": "02/14/2024"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user