1
0
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:
Jonathan Prusik 2025-01-06 16:36:32 -05:00
commit f408574322
No known key found for this signature in database
GPG Key ID: 83CF2DF735A5EC35
167 changed files with 6511 additions and 9369 deletions

View File

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

View File

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

View File

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

View File

@ -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": "拡張機能の幅"

View File

@ -4154,7 +4154,7 @@
"message": "Ek bilgiler"
},
"itemHistory": {
"message": "Öğe geçmişi"
"message": "Kayıt geçmişi"
},
"lastEdited": {
"message": "Son düzenlenme"

View File

@ -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": "您的密码库是空的"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
<bit-search
autocomplete="off"
[placeholder]="'search' | i18n"
[(ngModel)]="searchText"
(ngModelChange)="onSearchTextChanged()"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 || "&nbsp;" }}
</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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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": "تغيير البريد الإلكتروني الخاص بالحساب"

View File

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

View File

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

View File

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

View File

@ -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": "アカウントのメールアドレスを変更する"
}
}

View File

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

View File

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

View File

@ -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": "Уређај поуздан"

View File

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

View File

@ -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": "开启两步登录"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,2 @@
export * from "./shared.module";
export * from "./loose-components.module";
export * from "./components/index";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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