diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 2a7f18ca01..a95f11ffa1 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -226,7 +226,7 @@ "message": "Help & feedback" }, "helpCenter": { - "message": "Bitwarden Help center" + "message": "Bitwarden Help centre" }, "communityForums": { "message": "Explore Bitwarden community forums" @@ -728,7 +728,7 @@ "message": "Change the application's colour theme." }, "themeDescAlt": { - "message": "Change the application's color theme. Applies to all logged in accounts." + "message": "Change the application's colour theme. Applies to all logged in accounts." }, "dark": { "message": "Dark", @@ -1165,7 +1165,7 @@ "message": "Show a recognizable image next to each login." }, "faviconDescAlt": { - "message": "Show a recognizable image next to each login. Applies to all logged in accounts." + "message": "Show a recognisable image next to each login. Applies to all logged in accounts." }, "enableBadgeCounter": { "message": "Show badge counter" @@ -1730,7 +1730,7 @@ "message": "An organization policy is affecting your ownership options." }, "personalOwnershipPolicyInEffectImports": { - "message": "An organization policy has blocked importing items into your individual vault." + "message": "An organisation policy has blocked importing items into your individual vault." }, "excludedDomains": { "message": "Excluded Domains" @@ -1990,7 +1990,7 @@ "message": "Your Master Password was recently changed by an administrator in your organization. In order to access the vault, you must update it now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, "updateWeakMasterPasswordWarning": { - "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." + "message": "Your master password does not meet one or more of your organisation policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic Enrollment" @@ -2006,11 +2006,11 @@ "description": "Used as a message within the notification bar when no folders are found" }, "orgPermissionsUpdatedMustSetPassword": { - "message": "Your organization permissions were updated, requiring you to set a master password.", + "message": "Your organisation permissions were updated, requiring you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "orgRequiresYouToSetPassword": { - "message": "Your organization requires you to set a master password.", + "message": "Your organisation requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "verificationRequired": { @@ -2037,7 +2037,7 @@ } }, "vaultTimeoutPolicyWithActionInEffect": { - "message": "Your organization policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s). Your vault timeout action is set to $ACTION$.", + "message": "Your organisation policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s). Your vault timeout action is set to $ACTION$.", "placeholders": { "hours": { "content": "$1", @@ -2054,7 +2054,7 @@ } }, "vaultTimeoutActionPolicyInEffect": { - "message": "Your organization policies have set your vault timeout action to $ACTION$.", + "message": "Your organisation policies have set your vault timeout action to $ACTION$.", "placeholders": { "action": { "content": "$1", @@ -2111,7 +2111,7 @@ "message": "Exporting Personal Vault" }, "exportingIndividualVaultDescription": { - "message": "Only the individual vault items associated with $EMAIL$ will be exported. Organization vault items will not be included. Only vault item information will be exported and will not include associated attachments.", + "message": "Only the individual vault items associated with $EMAIL$ will be exported. Organisation vault items will not be included. Only vault item information will be exported and will not include associated attachments.", "placeholders": { "email": { "content": "$1", @@ -2305,7 +2305,7 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Your organization policies have turned on auto-fill on page load." + "message": "Your organisation policies have turned on auto-fill on page load." }, "howToAutofill": { "message": "How to auto-fill" @@ -2377,7 +2377,7 @@ "message": "Approve with master password" }, "ssoIdentifierRequired": { - "message": "Organization SSO identifier is required." + "message": "Organisation SSO identifier is required." }, "eu": { "message": "EU", @@ -2688,7 +2688,7 @@ "message": "Total" }, "importWarning": { - "message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organization. Do you want to proceed?", + "message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organisation. Do you want to proceed?", "placeholders": { "organization": { "content": "$1", @@ -3007,10 +3007,10 @@ "message": "Passkey removed" }, "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + "message": "Notice: Unassigned organisation items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + "message": "Notice: On May 16, 2024, unassigned organisation items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." }, "unassignedItemsBannerCTAPartOne": { "message": "Assign these items to a collection from the", diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index e1199857c7..4ffbb147be 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -3,7 +3,7 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Bitwarden - Administrador de contraseñas", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { @@ -2962,27 +2962,27 @@ "description": "Label indicating the most common import formats" }, "overrideDefaultBrowserAutofillTitle": { - "message": "¿Quiere hacer de Bitwarden su gestor de contraseñas predeterminado?", + "message": "¿Hacer de Bitwarden su administrador de contraseñas predeterminado?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Pasar por alto esta opción puede causar conflictos entre el menú de relleno automático de Bitwarden y el del navegador.", + "message": "Pasar por alto esta opción puede causar conflictos entre el menú de autocompletar de Bitwarden y el de tu navegador.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { - "message": "Hacer de Bitwarden su gestor de contraseñas predeterminado", + "message": "Hacer de Bitwarden tu administrador de contraseñas predeterminado", "description": "Label for the setting that allows overriding the default browser autofill settings" }, "privacyPermissionAdditionNotGrantedTitle": { - "message": "No se pudo establecer Bitwarden como el gestor de contraseñas predeterminado", + "message": "No se puede establecer Bitwarden como el administrador de contraseñas predeterminado", "description": "Title for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "privacyPermissionAdditionNotGrantedDescription": { - "message": "Debe otorgar los permisos de privacidad del navegador a Bitwarden para establecerlo como gestor de contraseñas predeterminado.", + "message": "Debes otorgar permisos de privacidad del navegador a Bitwarden para establecerlo como administrador de contraseñas predeterminado.", "description": "Description for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "makeDefault": { - "message": "Predeterminar", + "message": "Establecer como predeterminado", "description": "Button text for the setting that allows overriding the default browser autofill settings" }, "saveCipherAttemptSuccess": { @@ -2998,7 +2998,7 @@ "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "Éxito" }, "removePasskey": { "message": "Eliminar passkey" @@ -3013,20 +3013,20 @@ "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." }, "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", + "message": "Asignar estos elementos a una colección de", "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." }, "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", + "message": "para hcerlos visibles.", "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." }, "adminConsole": { - "message": "Admin Console" + "message": "Consola de administrador" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "Error al asignar la colección de destino." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "Error al asignar la carpeta de destino." } } diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index c167d89006..6ffbf522d3 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -173,10 +173,10 @@ "message": "Keisti pagrindinį slaptažodį" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Tęsti į žiniatinklio programėlę?" }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "Pagrindinį slaptažodį galite pakeisti „Bitwarden“ žiniatinklio programėlėje." }, "fingerprintPhrase": { "message": "Pirštų atspaudų frazė", @@ -2998,7 +2998,7 @@ "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "Sėkmė" }, "removePasskey": { "message": "Pašalinti slaptaraktį" @@ -3007,26 +3007,26 @@ "message": "Pašalintas slaptaraktis" }, "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + "message": "Pranešimas: nepriskirti organizacijos elementai nebėra matomi peržiūros rodinyje Visi saugyklos ir yra pasiekiami tik per Administratoriaus konsolę." }, "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + "message": "Pranešimas: 2024 m. gegužės 16 d. nepriskirti organizacijos elementai nebėra matomi peržiūros rodinyje Visi saugyklos ir yra pasiekiami tik per Administratoriaus konsolę." }, "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", + "message": "Priskirkite šiuos elementus kolekcijai iš", "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." }, "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", + "message": ", kad jie būtų matomi.", "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." }, "adminConsole": { - "message": "Admin Console" + "message": "Administratoriaus konsolės" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "Klaida priskiriant tikslinę kolekciją." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "Klaida priskiriant tikslinį aplanką." } } diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 989ed0786e..f2f50dc3ef 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1,4 +1,4 @@ -import { Subject, firstValueFrom, map, merge, timeout } from "rxjs"; +import { Subject, filter, firstValueFrom, map, merge, timeout } from "rxjs"; import { PinCryptoServiceAbstraction, @@ -1201,31 +1201,46 @@ export default class MainBackground { } async logout(expired: boolean, userId?: UserId) { - userId ??= ( - await firstValueFrom( - this.accountService.activeAccount$.pipe( - timeout({ - first: 2000, - with: () => { - throw new Error("No active account found to logout"); - }, - }), - ), - ) - )?.id; + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe( + map((a) => a?.id), + timeout({ + first: 2000, + with: () => { + throw new Error("No active account found to logout"); + }, + }), + ), + ); - await this.eventUploadService.uploadEvents(userId as UserId); + const userBeingLoggedOut = userId ?? activeUserId; + + await this.eventUploadService.uploadEvents(userBeingLoggedOut); + + // HACK: We shouldn't wait for the authentication status to change but instead subscribe to the + // authentication status to do various actions. + const logoutPromise = firstValueFrom( + this.authService.authStatusFor$(userBeingLoggedOut).pipe( + filter((authenticationStatus) => authenticationStatus === AuthenticationStatus.LoggedOut), + timeout({ + first: 5_000, + with: () => { + throw new Error("The logout process did not complete in a reasonable amount of time."); + }, + }), + ), + ); await Promise.all([ - this.syncService.setLastSync(new Date(0), userId), - this.cryptoService.clearKeys(userId), - this.cipherService.clear(userId), - this.folderService.clear(userId), - this.collectionService.clear(userId), - this.passwordGenerationService.clear(userId), - this.vaultTimeoutSettingsService.clear(userId), + this.syncService.setLastSync(new Date(0), userBeingLoggedOut), + this.cryptoService.clearKeys(userBeingLoggedOut), + this.cipherService.clear(userBeingLoggedOut), + this.folderService.clear(userBeingLoggedOut), + this.collectionService.clear(userBeingLoggedOut), + this.passwordGenerationService.clear(userBeingLoggedOut), + this.vaultTimeoutSettingsService.clear(userBeingLoggedOut), this.vaultFilterService.clear(), - this.biometricStateService.logout(userId), + this.biometricStateService.logout(userBeingLoggedOut), /* We intentionally do not clear: * - autofillSettingsService * - badgeSettingsService @@ -1236,20 +1251,28 @@ export default class MainBackground { //Needs to be checked before state is cleaned const needStorageReseed = await this.needsStorageReseed(); - const newActiveUser = await firstValueFrom( - this.accountService.nextUpAccount$.pipe(map((a) => a?.id)), - ); - await this.stateService.clean({ userId: userId }); - await this.accountService.clean(userId); + const newActiveUser = + userBeingLoggedOut === activeUserId + ? await firstValueFrom(this.accountService.nextUpAccount$.pipe(map((a) => a?.id))) + : null; - await this.stateEventRunnerService.handleEvent("logout", userId); + await this.stateService.clean({ userId: userBeingLoggedOut }); + await this.accountService.clean(userBeingLoggedOut); + await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut); + + // HACK: Wait for the user logging outs authentication status to transition to LoggedOut + await logoutPromise; + + await this.switchAccount(newActiveUser); if (newActiveUser != null) { // we have a new active user, do not continue tearing down application - await this.switchAccount(newActiveUser as UserId); this.messagingService.send("switchAccountFinish"); } else { - this.messagingService.send("doneLoggingOut", { expired: expired, userId: userId }); + this.messagingService.send("doneLoggingOut", { + expired: expired, + userId: userBeingLoggedOut, + }); } if (needStorageReseed) { diff --git a/apps/browser/store/locales/en_GB/copy.resx b/apps/browser/store/locales/en_GB/copy.resx index 82e4eb1d88..7c408ad889 100644 --- a/apps/browser/store/locales/en_GB/copy.resx +++ b/apps/browser/store/locales/en_GB/copy.resx @@ -124,7 +124,7 @@ At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! + Recognised as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! SECURE YOUR DIGITAL LIFE Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. @@ -146,7 +146,7 @@ More reasons to choose Bitwarden: World-Class Encryption Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -3rd-party Audits +3rd-Party Audits Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. Advanced 2FA @@ -159,13 +159,13 @@ Built-in Generator Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. Global Translations -Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. +Bitwarden translations exist for more than 60 languages, translated by the global community through Crowdin. Cross-Platform Applications -Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, desktop OS, and more. Bitwarden secures more than just passwords -End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! +End-to-end encrypted credential management solutions from Bitwarden empower organisations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! diff --git a/apps/browser/store/locales/en_IN/copy.resx b/apps/browser/store/locales/en_IN/copy.resx index 82e4eb1d88..31e5d2326f 100644 --- a/apps/browser/store/locales/en_IN/copy.resx +++ b/apps/browser/store/locales/en_IN/copy.resx @@ -133,7 +133,7 @@ ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE -Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. +Utilise Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. EMPOWER YOUR TEAMS WITH BITWARDEN Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. @@ -165,7 +165,7 @@ Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. Bitwarden secures more than just passwords -End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! +End-to-end encrypted credential management solutions from Bitwarden empower organisations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! diff --git a/apps/browser/store/locales/es/copy.resx b/apps/browser/store/locales/es/copy.resx index 472697d825..019006422a 100644 --- a/apps/browser/store/locales/es/copy.resx +++ b/apps/browser/store/locales/es/copy.resx @@ -118,10 +118,10 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden Password Manager + Bitwarden - Administrador de contraseñas - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + En casa, en el trabajo o en el viaje, Bitwarden asegura fácilmente todas sus contraseñas, claves de acceso e información confidencial. Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! @@ -169,7 +169,7 @@ End-to-end encrypted credential management solutions from Bitwarden empower orga - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + En casa, en el trabajo o mientras viaja, Bitwarden protege fácilmente todas sus contraseñas, claves de acceso e información confidencial. Sincroniza y accede a tu caja fuerte desde múltiples dispositivos diff --git a/apps/browser/store/locales/pt_PT/copy.resx b/apps/browser/store/locales/pt_PT/copy.resx index 34461983bc..65b81e2d2d 100644 --- a/apps/browser/store/locales/pt_PT/copy.resx +++ b/apps/browser/store/locales/pt_PT/copy.resx @@ -165,7 +165,7 @@ Aplicações multiplataforma Proteja e partilhe dados confidenciais dentro do seu cofre Bitwarden a partir de qualquer navegador, dispositivo móvel, ou SO de computador, e muito mais. O Bitwarden protege mais do que apenas palavras-passe -As soluções de gestão de credenciais encriptadas ponto a ponto do Bitwarden permitem que as organizações protejam tudo, incluindo segredos de programadores e experiências com chaves de acesso. Visite Bitwarden.com para saber mais sobre o Gestor de Segredos do Bitwarden e o Bitwarden Passwordless.dev! +As soluções de gestão de credenciais encriptadas ponto a ponto do Bitwarden permitem que as organizações protejam tudo, incluindo segredos de programadores e experiências com chaves de acesso. Visite Bitwarden.com para saber mais sobre o Bitwarden - Gestor de Segredos e o Bitwarden Passwordless.dev! diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index d060792c1a..cd9e381026 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -8,7 +8,7 @@ import { ViewContainerRef, } from "@angular/core"; import { Router } from "@angular/router"; -import { firstValueFrom, map, Subject, takeUntil } from "rxjs"; +import { filter, firstValueFrom, map, Subject, takeUntil, timeout } from "rxjs"; import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -567,7 +567,9 @@ export class AppComponent implements OnInit, OnDestroy { this.messagingService.send("updateAppMenu", { updateRequest: updateRequest }); } - private async logOut(expired: boolean, userId?: string, reason?: LogoutReason) { + // Even though the userId parameter is no longer optional doesn't mean a message couldn't be + // passing null-ish values to us. + private async logOut(expired: boolean, userId: UserId, reason?: LogoutReason) { if (reason) { this.toastService.showToast({ variant: "error", @@ -578,18 +580,39 @@ export class AppComponent implements OnInit, OnDestroy { await new Promise((resolve) => setTimeout(resolve, 5000)); } - const userBeingLoggedOut = - (userId as UserId) ?? - (await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id)))); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + + const userBeingLoggedOut = userId ?? activeUserId; // Mark account as being cleaned up so that the updateAppMenu logic (executed on syncCompleted) // doesn't attempt to update a user that is being logged out as we will manually // call updateAppMenu when the logout is complete. this.startAccountCleanUp(userBeingLoggedOut); - let preLogoutActiveUserId; - const nextUpAccount = await firstValueFrom(this.accountService.nextUpAccount$); + const nextUpAccount = + activeUserId === userBeingLoggedOut + ? await firstValueFrom(this.accountService.nextUpAccount$) // We'll need to switch accounts + : null; + try { + // HACK: We shouldn't wait for authentication status to change here but instead subscribe to the + // authentication status to do various actions. + const logoutPromise = firstValueFrom( + this.authService.authStatusFor$(userBeingLoggedOut).pipe( + filter((authenticationStatus) => authenticationStatus === AuthenticationStatus.LoggedOut), + timeout({ + first: 5_000, + with: () => { + throw new Error( + "The logout process did not complete in a reasonable amount of time.", + ); + }, + }), + ), + ); + // Provide the userId of the user to upload events for await this.eventUploadService.uploadEvents(userBeingLoggedOut); await this.syncService.setLastSync(new Date(0), userBeingLoggedOut); @@ -603,26 +626,33 @@ export class AppComponent implements OnInit, OnDestroy { await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut); - preLogoutActiveUserId = this.activeUserId; await this.stateService.clean({ userId: userBeingLoggedOut }); await this.accountService.clean(userBeingLoggedOut); + + // HACK: Wait for the user logging outs authentication status to transition to LoggedOut + await logoutPromise; } finally { this.finishAccountCleanUp(userBeingLoggedOut); } - if (nextUpAccount == 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(["login"]); - } else if (preLogoutActiveUserId !== nextUpAccount.id) { - this.messagingService.send("switchAccount", { userId: nextUpAccount.id }); + // We only need to change the display at all if the account being looked at is the one + // being logged out. If it was a background account, no need to do anything. + if (userBeingLoggedOut === activeUserId) { + if (nextUpAccount != null) { + this.messagingService.send("switchAccount", { userId: nextUpAccount.id }); + } else { + // We don't have another user to switch to, bring them to the login page so they + // can sign into a user. + await this.accountService.switchAccount(null); + void this.router.navigate(["login"]); + } } await this.updateAppMenu(); // This must come last otherwise the logout will prematurely trigger // a process reload before all the state service user data can be cleaned up - if (userBeingLoggedOut === preLogoutActiveUserId) { + if (userBeingLoggedOut === activeUserId) { this.authService.logOut(async () => { if (expired) { this.platformUtilsService.showToast( @@ -713,7 +743,7 @@ export class AppComponent implements OnInit, OnDestroy { // 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 options[1] === "logOut" - ? this.logOut(false, userId) + ? this.logOut(false, userId as UserId) : await this.vaultTimeoutService.lock(userId); } } diff --git a/apps/desktop/src/app/layout/account-switcher.component.ts b/apps/desktop/src/app/layout/account-switcher.component.ts index c8a26065c1..92cfebfd60 100644 --- a/apps/desktop/src/app/layout/account-switcher.component.ts +++ b/apps/desktop/src/app/layout/account-switcher.component.ts @@ -92,6 +92,11 @@ export class AccountSwitcherComponent { return null; } + if (!active.name && !active.email) { + // We need to have this information at minimum to display them. + return null; + } + return { id: active.id, name: active.name, diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index 5c8c32b7c1..5572d2fd35 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -1549,11 +1549,11 @@ "message": "Set master password" }, "orgPermissionsUpdatedMustSetPassword": { - "message": "Your organization permissions were updated, requiring you to set a master password.", + "message": "Your organisation permissions were updated, requiring you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "orgRequiresYouToSetPassword": { - "message": "Your organization requires you to set a master password.", + "message": "Your organisation requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "verificationRequired": { @@ -1699,7 +1699,7 @@ "message": "An organisation policy is affecting your ownership options." }, "personalOwnershipPolicyInEffectImports": { - "message": "An organization policy has blocked importing items into your individual vault." + "message": "An organisation policy has blocked importing items into your individual vault." }, "allSends": { "message": "All Sends", @@ -1895,7 +1895,7 @@ "message": "Your master password was recently changed by an administrator in your organisation. In order to access the vault, you must update it now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, "updateWeakMasterPasswordWarning": { - "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." + "message": "Your master password does not meet one or more of your organisation policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, "tryAgain": { "message": "Try again" @@ -1953,7 +1953,7 @@ } }, "vaultTimeoutPolicyWithActionInEffect": { - "message": "Your organization policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s). Your vault timeout action is set to $ACTION$.", + "message": "Your organisation policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s). Your vault timeout action is set to $ACTION$.", "placeholders": { "hours": { "content": "$1", @@ -1970,7 +1970,7 @@ } }, "vaultTimeoutActionPolicyInEffect": { - "message": "Your organization policies have set your vault timeout action to $ACTION$.", + "message": "Your organisation policies have set your vault timeout action to $ACTION$.", "placeholders": { "action": { "content": "$1", @@ -2060,7 +2060,7 @@ "message": "Exporting individual vault" }, "exportingIndividualVaultDescription": { - "message": "Only the individual vault items associated with $EMAIL$ will be exported. Organization vault items will not be included. Only vault item information will be exported and will not include associated attachments.", + "message": "Only the individual vault items associated with $EMAIL$ will be exported. Organisation vault items will not be included. Only vault item information will be exported and will not include associated attachments.", "placeholders": { "email": { "content": "$1", @@ -2346,7 +2346,7 @@ "message": "Region" }, "ssoIdentifierRequired": { - "message": "Organization SSO identifier is required." + "message": "Organisation SSO identifier is required." }, "eu": { "message": "EU", @@ -2532,7 +2532,7 @@ "message": "Total" }, "importWarning": { - "message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organization. Do you want to proceed?", + "message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organisation. Do you want to proceed?", "placeholders": { "organization": { "content": "$1", diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index abfa0b1c0d..79f2de8f63 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -1549,11 +1549,11 @@ "message": "Set master password" }, "orgPermissionsUpdatedMustSetPassword": { - "message": "Your organization permissions were updated, requiring you to set a master password.", + "message": "Your organisation permissions were updated, requiring you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "orgRequiresYouToSetPassword": { - "message": "Your organization requires you to set a master password.", + "message": "Your organisation requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "verificationRequired": { @@ -1699,7 +1699,7 @@ "message": "An organization policy is affecting your ownership options." }, "personalOwnershipPolicyInEffectImports": { - "message": "An organization policy has blocked importing items into your individual vault." + "message": "An organisation policy has blocked importing items into your individual vault." }, "allSends": { "message": "All Sends", @@ -1895,7 +1895,7 @@ "message": "Your master password was recently changed by an administrator in your organization. In order to access the vault, you must update it now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, "updateWeakMasterPasswordWarning": { - "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." + "message": "Your master password does not meet one or more of your organisation policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, "tryAgain": { "message": "Try again" @@ -1953,7 +1953,7 @@ } }, "vaultTimeoutPolicyWithActionInEffect": { - "message": "Your organization policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s). Your vault timeout action is set to $ACTION$.", + "message": "Your organisation policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s). Your vault timeout action is set to $ACTION$.", "placeholders": { "hours": { "content": "$1", @@ -1970,7 +1970,7 @@ } }, "vaultTimeoutActionPolicyInEffect": { - "message": "Your organization policies have set your vault timeout action to $ACTION$.", + "message": "Your organisation policies have set your vault timeout action to $ACTION$.", "placeholders": { "action": { "content": "$1", @@ -2060,7 +2060,7 @@ "message": "Exporting individual vault" }, "exportingIndividualVaultDescription": { - "message": "Only the individual vault items associated with $EMAIL$ will be exported. Organization vault items will not be included. Only vault item information will be exported and will not include associated attachments.", + "message": "Only the individual vault items associated with $EMAIL$ will be exported. Organisation vault items will not be included. Only vault item information will be exported and will not include associated attachments.", "placeholders": { "email": { "content": "$1", @@ -2346,7 +2346,7 @@ "message": "Region" }, "ssoIdentifierRequired": { - "message": "Organization SSO identifier is required." + "message": "Organisation SSO identifier is required." }, "eu": { "message": "EU", @@ -2532,7 +2532,7 @@ "message": "Total" }, "importWarning": { - "message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organization. Do you want to proceed?", + "message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organisation. Do you want to proceed?", "placeholders": { "organization": { "content": "$1", diff --git a/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts b/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts index 80d77968f2..8cf7ed313f 100644 --- a/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts @@ -1,6 +1,6 @@ import { Component } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { concatMap, takeUntil, map } from "rxjs"; +import { concatMap, takeUntil, map, lastValueFrom } from "rxjs"; import { tap } from "rxjs/operators"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -8,12 +8,15 @@ import { ApiService } from "@bitwarden/common/abstractions/api.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 { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; +import { TwoFactorDuoResponse } from "@bitwarden/common/auth/models/response/two-factor-duo.response"; +import { AuthResponse } from "@bitwarden/common/auth/types/auth-response"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { DialogService } from "@bitwarden/components"; import { TwoFactorDuoComponent } from "../../../auth/settings/two-factor-duo.component"; import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from "../../../auth/settings/two-factor-setup.component"; +import { TwoFactorVerifyComponent } from "../../../auth/settings/two-factor-verify.component"; @Component({ selector: "app-two-factor-setup", @@ -63,9 +66,18 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent { async manage(type: TwoFactorProviderType) { switch (type) { case TwoFactorProviderType.OrganizationDuo: { + const twoFactorVerifyDialogRef = TwoFactorVerifyComponent.open(this.dialogService, { + data: { type: type, organizationId: this.organizationId }, + }); + const result: AuthResponse = await lastValueFrom( + twoFactorVerifyDialogRef.closed, + ); + if (!result) { + return; + } + const duoComp = await this.openModal(this.duoModalRef, TwoFactorDuoComponent); - duoComp.type = TwoFactorProviderType.OrganizationDuo; - duoComp.organizationId = this.organizationId; + duoComp.auth(result); duoComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => { this.updateStatus(enabled, TwoFactorProviderType.OrganizationDuo); }); diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index 1939bb11f5..6e2761a9c4 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -2,7 +2,7 @@ import { DOCUMENT } from "@angular/common"; import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core"; import { NavigationEnd, Router } from "@angular/router"; import * as jq from "jquery"; -import { Subject, firstValueFrom, map, switchMap, takeUntil, timer } from "rxjs"; +import { Subject, filter, firstValueFrom, map, switchMap, takeUntil, timeout, timer } from "rxjs"; import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; @@ -13,6 +13,7 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { PaymentMethodWarningsServiceAbstraction as PaymentMethodWarningService } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -136,9 +137,7 @@ export class AppComponent implements OnDestroy, OnInit { this.router.navigate(["/"]); break; case "logout": - // 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.logOut(!!message.expired, message.redirect); + await this.logOut(!!message.expired, message.redirect); break; case "lockVault": await this.vaultTimeoutService.lock(); @@ -266,7 +265,20 @@ export class AppComponent implements OnDestroy, OnInit { private async logOut(expired: boolean, redirect = true) { await this.eventUploadService.uploadEvents(); - const userId = await this.stateService.getUserId(); + const userId = (await this.stateService.getUserId()) as UserId; + + const logoutPromise = firstValueFrom( + this.authService.authStatusFor$(userId).pipe( + filter((authenticationStatus) => authenticationStatus === AuthenticationStatus.LoggedOut), + timeout({ + first: 5_000, + with: () => { + throw new Error("The logout process did not complete in a reasonable amount of time."); + }, + }), + ), + ); + await Promise.all([ this.syncService.setLastSync(new Date(0)), this.cryptoService.clearKeys(), @@ -274,11 +286,11 @@ export class AppComponent implements OnDestroy, OnInit { this.folderService.clear(userId), this.collectionService.clear(userId), this.passwordGenerationService.clear(), - this.biometricStateService.logout(userId as UserId), + this.biometricStateService.logout(userId), this.paymentMethodWarningService.clear(), ]); - await this.stateEventRunnerService.handleEvent("logout", userId as UserId); + await this.stateEventRunnerService.handleEvent("logout", userId); await this.searchService.clearIndex(); this.authService.logOut(async () => { @@ -291,6 +303,10 @@ export class AppComponent implements OnDestroy, OnInit { } await this.stateService.clean({ userId: userId }); + await this.accountService.clean(userId); + + await logoutPromise; + if (redirect) { // 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 diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index 2c05de9954..fd873a41f1 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index 9353797bb4..bb99982684 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index 19e38577a4..795b6bee8e 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "$ID$ silinməzdən əvvəl bütün müştəriləri (client) ayırmalısınız", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index de563800ea..ba424f456b 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index f5465fb658..5b071f6658 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "Трябва да разкачите всички клиенти, преди да можете да изтриете $ID$", + "deleteProviderWarningDescription": { + "message": "Трябва да разкачите всички клиенти, преди да можете да изтриете $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index 1b21a9a04d..0de85f1b9f 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index 2382a31caf..de846aa801 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index 80fd0802d3..443348f1e0 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "Heu de desenllaçar tots els clients abans de poder suprimir $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index 32f1f68d45..ae9df7bb1d 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "Před smazáním $ID$ musíte odpojit všechny klienty", + "deleteProviderWarningDescription": { + "message": "Před smazáním $ID$ musíte odpojit všechny klienty.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index 09cc963d67..791f940f11 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index 30ba2d09e4..f4b6559cfc 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "Alle klienttilknytninger skal fjernes, før $ID$ kan slettes", + "deleteProviderWarningDescription": { + "message": "Alle klienttilknytninger skal fjernes, før $ID$ kan slettes.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index 1f520f546d..f7d0e08273 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -6481,7 +6481,7 @@ "message": "Gewähre Zugriff auf Sammlungen, indem du diese zu dieser Gruppe hinzufügst." }, "editGroupCollectionsRestrictionsDesc": { - "message": "You can only assign collections you manage." + "message": "Du kannst nur von dir verwaltete Sammlungen zuweisen." }, "accessAllCollectionsDesc": { "message": "Gewähre Zugriff auf alle aktuellen und zukünftigen Sammlungen." @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "Du musst die Verknüpfung zu allen Kunden aufheben, bevor du $ID$ löschen kannst", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index b57f8728e5..6dab7f4816 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index f5fff9a81d..4840003abd 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index 6910230fc4..bf746dd0d5 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index 14d1b49c83..3343910c8a 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index 51374f8432..4f9ffd2f0f 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index d49dbb6000..338d172bb7 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -579,7 +579,7 @@ "message": "Acceso" }, "accessLevel": { - "message": "Access level" + "message": "Nivel de acceso" }, "loggedOut": { "message": "Sesión terminada" @@ -1359,11 +1359,11 @@ "description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead. (Optional second half: You may need to wait until your administrator confirms your organization membership.)" }, "onboardingImportDataDetailsPartTwoNoOrgs": { - "message": " instead.", + "message": " en su lugar.", "description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead." }, "onboardingImportDataDetailsPartTwoWithOrgs": { - "message": " instead. You may need to wait until your administrator confirms your organization membership.", + "message": " en su lugar. Es posible que tenga que esperar hasta que su administrador confirme la pertenencia a su organización.", "description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead. You may need to wait until your administrator confirms your organization membership." }, "importError": { @@ -1810,7 +1810,7 @@ "message": "Sitios web no seguros encontrados" }, "unsecuredWebsitesFoundReportDesc": { - "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "message": "Hemos encontrado $COUNT$ elementos en su $VAULT$ con URIs no seguros. Deberías cambiar su esquema URI a https:// si el sitio web lo permite.", "placeholders": { "count": { "content": "$1", @@ -1835,7 +1835,7 @@ "message": "Inicios de sesión sin 2FA encontrados" }, "inactive2faFoundReportDesc": { - "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "message": "Hemos encontrado $COUNT$ sitio(s) web en su $VAULT$ que pueden no estar configurados con inicio de sesión en dos pasos (según 2fa.directory). Para proteger aún más estas cuentas, debe configurar el inicio de sesión en dos pasos.", "placeholders": { "count": { "content": "$1", @@ -1863,7 +1863,7 @@ "message": "Contraseñas comprometidas encontradas" }, "exposedPasswordsFoundReportDesc": { - "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "message": "Hemos encontrado $COUNT$ elementos en su $VAULT$ que tienen contraseñas que fueron expuestas en violaciones de datos conocidas. Deberías cambiarlas para utilizar una contraseña nueva.", "placeholders": { "count": { "content": "$1", @@ -1900,7 +1900,7 @@ "message": "Contraseñas débiles encontradas" }, "weakPasswordsFoundReportDesc": { - "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", + "message": "Hemos encontrado $COUNT$ elementos en su $VAULT$ con contraseñas que no son seguras. Debe actualizarlos para utilizar contraseñas más seguras.", "placeholders": { "count": { "content": "$1", @@ -1925,7 +1925,7 @@ "message": "Contraseñas reutilizadas encontradas" }, "reusedPasswordsFoundReportDesc": { - "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", + "message": "Hemos encontrado $COUNT$ contraseñas que están siendo reutilizadas en tu $VAULT$. Deberías cambiarlas por un valor único.", "placeholders": { "count": { "content": "$1", @@ -4971,13 +4971,13 @@ "message": "Cree una nueva organización de clientes que estará asociada a usted como proveedor. Usted poddrá acceder y gestionar esta organización." }, "newClient": { - "message": "New client" + "message": "Nuevo cliente" }, "addExistingOrganization": { "message": "Añadir una organización existente" }, "addNewOrganization": { - "message": "Add new organization" + "message": "Añadir nueva organización" }, "myProvider": { "message": "Mi proveedor" @@ -5233,7 +5233,7 @@ "message": "Set a unique SP entity ID" }, "spUniqueEntityIdDesc": { - "message": "Generate an identifier that is unique to your organization" + "message": "Genera un identificador único para su organización" }, "idpEntityId": { "message": "ID de la entidad" @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index ef4d3f8ad3..d2ea371f43 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index 9509214f0e..9b208f945a 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index d8f2fd947a..45a32b2011 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index 17b629efd5..eb0e93114b 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -1810,7 +1810,7 @@ "message": "Suojaamattomia verkkosivustoja löytyi" }, "unsecuredWebsitesFoundReportDesc": { - "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "message": "Löysimme holv(e)istasi $COUNT$ kohdetta suojaamattomilla URI-osoitteilla. Nämä tulisi muuttaa suojattuun \"https://\" -muotoon, jos sivustot sen mahdollistavat.", "placeholders": { "count": { "content": "$1", @@ -1835,7 +1835,7 @@ "message": "Löytyi kirjautumistietoja, joille ei ole määritetty kaksivaiheista kirjautumista" }, "inactive2faFoundReportDesc": { - "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "message": "Löysimme holv(e)istasi $COUNT$ sivustoa, joita ei ehkä ole määritetty käyttämään kaksivaiheista tunnistautumista (2fa.directory-sivuston mukaan). Nämä tilit tulisi suojata paremmin määrittämällä niille kaksivaiheinen tunnistautuminen.", "placeholders": { "count": { "content": "$1", @@ -1863,7 +1863,7 @@ "message": "Paljastuneita salasanoja löytyi" }, "exposedPasswordsFoundReportDesc": { - "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "message": "Löysimme holv(e)istasi $COUNT$ kohdetta, joiden salasanat ovat paljastuneet tunnetuissa tietovuodoissa. Nämä salasanat tulisi vaihtaa.", "placeholders": { "count": { "content": "$1", @@ -1900,7 +1900,7 @@ "message": "Heikkoja salasanoja löytyi" }, "weakPasswordsFoundReportDesc": { - "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", + "message": "Löysimme holv(e)istasi $COUNT$ kohdetta, joiden salasanat eivät ole vahvoja. Nämä tulisi korvata vahvemmilla salasanoilla.", "placeholders": { "count": { "content": "$1", @@ -1925,7 +1925,7 @@ "message": "Toistuvia salasanoja löytyi" }, "reusedPasswordsFoundReportDesc": { - "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", + "message": "Löysimme holv(e)istasi $COUNT$ toistuvasti käytettyä salasanaa. Ne tulisi korvata yksilöllisillä salasanoilla.", "placeholders": { "count": { "content": "$1", @@ -6481,7 +6481,7 @@ "message": "Myönnä käyttöoikeudet kokoelmiin lisäämällä heidät tähän ryhmään." }, "editGroupCollectionsRestrictionsDesc": { - "message": "You can only assign collections you manage." + "message": "Voit määrittää vain hallitsemiasi kokoelmia." }, "accessAllCollectionsDesc": { "message": "Myönnä käyttöoikeudet kaikkiin nykyisiin ja tuleviin kokoelmiin" @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "Kaikki liitetyt asiakkaat on poistettava ennen toimittajan $ID$ poistoa.", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", @@ -8074,6 +8074,6 @@ "message": "Valitse kokoelman kohde" }, "manageBillingFromProviderPortalMessage": { - "message": "Manage billing from the Provider Portal" + "message": "Hallitse laskutusta Toimittajaportaaliista" } } diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index aed2665fcc..e3806b6541 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index bd0144d7a5..eac5e78d87 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "Vous devez dissocier tous les clients avant de pouvoir supprimer $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index 2d7c32595c..417c7adc6a 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index e5a57df2c3..c448a25e59 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index 5439aee5e5..5e0d3b8c29 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index ed5fb33615..6eac81da08 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index ca5fa97736..1c99680f62 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "$ID$ törlése előtt le kell választani az összes klienst.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index dae66593f9..0aacc90be7 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index 9a55317978..5c3894d443 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -1863,7 +1863,7 @@ "message": "Password esposte trovate" }, "exposedPasswordsFoundReportDesc": { - "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "message": "Abbiamo trovato $COUNT$ elementi nella tua cassaforte che hanno password che sono state esposte a violazioni di dati note. Dovresti cambiarli per usare una nuova password.", "placeholders": { "count": { "content": "$1", @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "Devi scollegare tutti i client prima di poter eliminare $ID$", + "deleteProviderWarningDescription": { + "message": "Devi scollegare tutti i client prima di poter eliminare $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index b93dbe7a60..8bf56e6f65 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "$ID$ を削除する前に、すべてのクライアントのリンクを解除してください", + "deleteProviderWarningDescription": { + "message": "$ID$ を削除するには、まずすべてのクライアントのリンクを解除してください。", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index c05eab5314..104810e936 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index 2d7c32595c..417c7adc6a 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index a7f71b2ef9..3b34cc7976 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index 86695a392a..76b86a7433 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index bf47a1009d..cd5991ce7c 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "Ir jāatsaista visi klienti, pirms var izdzēst $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index d3a958a95b..3bd5ca3049 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index 2d7c32595c..417c7adc6a 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index 2d7c32595c..417c7adc6a 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index adb0dc144d..5b544ff3a4 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index 6590b35547..24aaf3d0d3 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index 17d396386c..5a203b5a51 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "Je moet alle clients ontkoppelen voordat je $ID$ kunt verwijderen", + "deleteProviderWarningDescription": { + "message": "Je moet alle clients ontkoppelen voordat je $ID$ kunt verwijderen.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index 35c43669a4..7569f5a4ee 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index 2d7c32595c..417c7adc6a 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index 66aacde732..e1a48f0fb0 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "Musisz odłączyć wszystkich klientów zanim będziesz mógł usunąć $ID$", + "deleteProviderWarningDescription": { + "message": "Musisz odłączyć wszystkich klientów zanim będziesz mógł usunąć $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index 7c94396334..fc3c4b1a51 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "Você deve desvincular todos os clientes antes de excluir $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index 38bc977d3a..5446a9dd65 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -1288,7 +1288,7 @@ "message": "Definições da chave de encriptação alteradas" }, "dangerZone": { - "message": "Zona de perigo" + "message": "Zona de risco" }, "dangerZoneDesc": { "message": "Cuidado, estas ações são irreversíveis!" @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "É necessário desvincular todos os clientes antes de poder eliminar $ID$", + "deleteProviderWarningDescription": { + "message": "É necessário desvincular todos os clientes antes de poder eliminar $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index 1d54a578d5..eb40a84da9 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index bc1ff8d6eb..92eced8799 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "Перед удалением $ID$ необходимо отвязать всех клиентов", + "deleteProviderWarningDescription": { + "message": "Перед удалением $ID$ необходимо отвязать всех клиентов.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index f74146210e..f264457250 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index c47022d785..7dd89c23be 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "Pred tým než budete môcť odstrániť $ID$, musíte odpojiť všetkých klientov", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index 8f8102d14e..5a85e934f0 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/sr/messages.json b/apps/web/src/locales/sr/messages.json index 1b060d0d3f..38bc66f911 100644 --- a/apps/web/src/locales/sr/messages.json +++ b/apps/web/src/locales/sr/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "Морате прекинути везу са свим клијентима да бисте могли да избришете $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index d4d435759a..ecc9093748 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index 427ea5f9c3..1d082c5eab 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index 2d7c32595c..417c7adc6a 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index 091660dd83..dd519daa29 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index b439f60538..2954942728 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index 9dcb10f101..23aa9b94b9 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "Перш ніж видалити $ID$, ви повинні від'єднати всіх клієнтів", + "deleteProviderWarningDescription": { + "message": "Перш ніж видалити $ID$, ви повинні від'єднати всіх клієнтів.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index 43718cd347..aa5ad2c9d3 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -579,7 +579,7 @@ "message": "Quyền truy cập" }, "accessLevel": { - "message": "Access level" + "message": "Cấp độ truy cập" }, "loggedOut": { "message": "Đã đăng xuất" @@ -609,7 +609,7 @@ "message": "Đăng nhập bằng thiết bị" }, "loginWithDeviceEnabledNote": { - "message": "Log in with device must be set up in the settings of the Bitwarden app. Need another option?" + "message": "Đăng nhập bằng thiết bị phải được thiết lập trong cài đặt của ứng dụng Bitwarden. Dùng cách khác?" }, "loginWithMasterPassword": { "message": "Đăng nhập bằng mật khẩu chính" @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index 0631291823..8e5a9e696b 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -7961,7 +7961,7 @@ } } }, - "deleteProviderWarningDesc": { + "deleteProviderWarningDescription": { "message": "删除 $ID$ 之前,您必须取消链接所有的客户端。", "placeholders": { "id": { @@ -7996,7 +7996,7 @@ "message": "集成" }, "integrationsDesc": { - "message": "通过 Bitwarden 机密管理器将机密自动同步到第三方服务。" + "message": "自动将机密从 Bitwarden 机密管理器同步到第三方服务。" }, "sdks": { "message": "SDK" diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index 5e4d8f5d48..c2b0060ac6 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -7961,8 +7961,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts index 0dace2945e..e2b8aeaffd 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts @@ -81,8 +81,9 @@ export class AccountComponent { if (providerClients.data != null && providerClients.data.length > 0) { await this.dialogService.openSimpleDialog({ title: { key: "deleteProviderName", placeholders: [this.provider.name] }, - content: { key: "deleteProviderWarningDesc", placeholders: [this.provider.name] }, + content: { key: "deleteProviderWarningDescription", placeholders: [this.provider.name] }, acceptButtonText: { key: "ok" }, + cancelButtonText: { key: "close" }, type: "danger", }); diff --git a/libs/angular/src/auth/components/set-password.component.ts b/libs/angular/src/auth/components/set-password.component.ts index 00a36434b0..7ddc76d6c1 100644 --- a/libs/angular/src/auth/components/set-password.component.ts +++ b/libs/angular/src/auth/components/set-password.component.ts @@ -244,7 +244,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts); await this.kdfConfigService.setKdfConfig(this.userId, this.kdfConfig); await this.masterPasswordService.setMasterKey(masterKey, this.userId); - await this.cryptoService.setUserKey(userKey[0]); + await this.cryptoService.setUserKey(userKey[0], this.userId); // Set private key only for new JIT provisioned users in MP encryption orgs // Existing TDE users will have private key set on sync or on login @@ -253,7 +253,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { this.forceSetPasswordReason != ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission ) { - await this.cryptoService.setPrivateKey(keyPair[1].encryptedString); + await this.cryptoService.setPrivateKey(keyPair[1].encryptedString, this.userId); } const localMasterKeyHash = await this.cryptoService.hashMasterKey( diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts index 0efb9569eb..cde64f0477 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts @@ -140,7 +140,7 @@ describe("AuthRequestLoginStrategy", () => { expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key); expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey); expect(deviceTrustService.trustDeviceIfRequired).toHaveBeenCalled(); - expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey); + expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, mockUserId); }); it("sets keys after a successful authentication when only userKey provided in login credentials", async () => { @@ -164,7 +164,7 @@ describe("AuthRequestLoginStrategy", () => { // setMasterKeyEncryptedUserKey, setUserKey, and setPrivateKey should still be called expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key); expect(cryptoService.setUserKey).toHaveBeenCalledWith(decUserKey); - expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey); + expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, mockUserId); // trustDeviceIfRequired should be called expect(deviceTrustService.trustDeviceIfRequired).not.toHaveBeenCalled(); diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts index c8acc6c24b..e815d8f3ba 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts @@ -161,9 +161,13 @@ export class AuthRequestLoginStrategy extends LoginStrategy { } } - protected override async setPrivateKey(response: IdentityTokenResponse): Promise { + protected override async setPrivateKey( + response: IdentityTokenResponse, + userId: UserId, + ): Promise { await this.cryptoService.setPrivateKey( - response.privateKey ?? (await this.createKeyPairForOldAccount()), + response.privateKey ?? (await this.createKeyPairForOldAccount(userId)), + userId, ); } diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index 96f7b73cab..adcf753325 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -252,7 +252,7 @@ export abstract class LoginStrategy { await this.setMasterKey(response, userId); await this.setUserKey(response, userId); - await this.setPrivateKey(response); + await this.setPrivateKey(response, userId); this.messagingService.send("loggedIn"); @@ -262,7 +262,7 @@ export abstract class LoginStrategy { // The keys comes from different sources depending on the login strategy protected abstract setMasterKey(response: IdentityTokenResponse, userId: UserId): Promise; protected abstract setUserKey(response: IdentityTokenResponse, userId: UserId): Promise; - protected abstract setPrivateKey(response: IdentityTokenResponse): Promise; + protected abstract setPrivateKey(response: IdentityTokenResponse, userId: UserId): Promise; // Old accounts used master key for encryption. We are forcing migrations but only need to // check on password logins @@ -270,9 +270,10 @@ export abstract class LoginStrategy { return false; } - protected async createKeyPairForOldAccount() { + protected async createKeyPairForOldAccount(userId: UserId) { try { - const [publicKey, privateKey] = await this.cryptoService.makeKeyPair(); + const userKey = await this.cryptoService.getUserKeyWithLegacySupport(userId); + const [publicKey, privateKey] = await this.cryptoService.makeKeyPair(userKey); await this.apiService.postAccountKeys(new KeysRequest(publicKey, privateKey.encryptedString)); return privateKey.encryptedString; } catch (e) { diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts index e85d01c5d7..f887f047a9 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts @@ -178,7 +178,7 @@ describe("PasswordLoginStrategy", () => { userId, ); expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey, userId); - expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey); + expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, userId); }); it("does not force the user to update their master password when there are no requirements", async () => { diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.ts b/libs/auth/src/common/login-strategies/password-login.strategy.ts index ec2a4850fe..80048d6e10 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.ts @@ -233,9 +233,13 @@ export class PasswordLoginStrategy extends LoginStrategy { } } - protected override async setPrivateKey(response: IdentityTokenResponse): Promise { + protected override async setPrivateKey( + response: IdentityTokenResponse, + userId: UserId, + ): Promise { await this.cryptoService.setPrivateKey( - response.privateKey ?? (await this.createKeyPairForOldAccount()), + response.privateKey ?? (await this.createKeyPairForOldAccount(userId)), + userId, ); } diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.ts index 37f616c98a..f328547772 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.ts @@ -354,12 +354,16 @@ export class SsoLoginStrategy extends LoginStrategy { await this.cryptoService.setUserKey(userKey); } - protected override async setPrivateKey(tokenResponse: IdentityTokenResponse): Promise { + protected override async setPrivateKey( + tokenResponse: IdentityTokenResponse, + userId: UserId, + ): Promise { const newSsoUser = tokenResponse.key == null; if (!newSsoUser) { await this.cryptoService.setPrivateKey( - tokenResponse.privateKey ?? (await this.createKeyPairForOldAccount()), + tokenResponse.privateKey ?? (await this.createKeyPairForOldAccount(userId)), + userId, ); } } diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts index 03dfa4f5fa..673fadd5b0 100644 --- a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts @@ -159,7 +159,7 @@ describe("UserApiLoginStrategy", () => { await apiLogInStrategy.logIn(credentials); expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key); - expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey); + expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, userId); }); it("gets and sets the master key if Key Connector is enabled", async () => { diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.ts index b9d0c9e588..440dccd12e 100644 --- a/libs/auth/src/common/login-strategies/user-api-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.ts @@ -116,9 +116,13 @@ export class UserApiLoginStrategy extends LoginStrategy { } } - protected override async setPrivateKey(response: IdentityTokenResponse): Promise { + protected override async setPrivateKey( + response: IdentityTokenResponse, + userId: UserId, + ): Promise { await this.cryptoService.setPrivateKey( - response.privateKey ?? (await this.createKeyPairForOldAccount()), + response.privateKey ?? (await this.createKeyPairForOldAccount(userId)), + userId, ); } diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts index 9e977193dd..afac2c2e6a 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts @@ -224,7 +224,7 @@ describe("WebAuthnLoginStrategy", () => { mockPrfPrivateKey, ); expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockUserKey, userId); - expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(idTokenResponse.privateKey); + expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(idTokenResponse.privateKey, userId); // Master key and private key should not be set expect(masterPasswordService.mock.setMasterKey).not.toHaveBeenCalled(); diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts index 714edabc1e..4b5441d00a 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts @@ -139,9 +139,13 @@ export class WebAuthnLoginStrategy extends LoginStrategy { } } - protected override async setPrivateKey(response: IdentityTokenResponse): Promise { + protected override async setPrivateKey( + response: IdentityTokenResponse, + userId: UserId, + ): Promise { await this.cryptoService.setPrivateKey( - response.privateKey ?? (await this.createKeyPairForOldAccount()), + response.privateKey ?? (await this.createKeyPairForOldAccount(userId)), + userId, ); } diff --git a/libs/common/src/auth/services/key-connector.service.ts b/libs/common/src/auth/services/key-connector.service.ts index 5065e58c76..65d1030bd3 100644 --- a/libs/common/src/auth/services/key-connector.service.ts +++ b/libs/common/src/auth/services/key-connector.service.ts @@ -155,7 +155,7 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { await this.cryptoService.setUserKey(userKey[0], userId); await this.cryptoService.setMasterKeyEncryptedUserKey(userKey[1].encryptedString, userId); - const [pubKey, privKey] = await this.cryptoService.makeKeyPair(); + const [pubKey, privKey] = await this.cryptoService.makeKeyPair(userKey[0]); try { const keyConnectorUrl = diff --git a/libs/common/src/models/response/profile.response.ts b/libs/common/src/models/response/profile.response.ts index fbaa4f84ef..3d5fde9ac4 100644 --- a/libs/common/src/models/response/profile.response.ts +++ b/libs/common/src/models/response/profile.response.ts @@ -1,11 +1,12 @@ import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response"; import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response"; import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response"; +import { UserId } from "../../types/guid"; import { BaseResponse } from "./base.response"; export class ProfileResponse extends BaseResponse { - id: string; + id: UserId; name: string; email: string; emailVerified: boolean; diff --git a/libs/common/src/platform/abstractions/crypto.service.ts b/libs/common/src/platform/abstractions/crypto.service.ts index 0a210e6709..43f47b0a3d 100644 --- a/libs/common/src/platform/abstractions/crypto.service.ts +++ b/libs/common/src/platform/abstractions/crypto.service.ts @@ -54,13 +54,23 @@ export abstract class CryptoService { * for encryption of data instead of the user key. */ abstract isLegacyUser(masterKey?: MasterKey, userId?: string): Promise; + + /** + * Use for encryption/decryption of data in order to support legacy + * encryption models. It will return the user key if available, + * if not it will return the master key. + * + * @deprecated Please provide the userId of the user you want the user key for. + */ + abstract getUserKeyWithLegacySupport(): Promise; + /** * Use for encryption/decryption of data in order to support legacy * encryption models. It will return the user key if available, * if not it will return the master key. * @param userId The desired user */ - abstract getUserKeyWithLegacySupport(userId?: string): Promise; + abstract getUserKeyWithLegacySupport(userId: UserId): Promise; /** * Retrieves the user key from storage * @param keySuffix The desired version of the user's key to retrieve @@ -169,10 +179,12 @@ export abstract class CryptoService { * organization keys currently in memory * @param orgs The organizations to set keys for * @param providerOrgs The provider organizations to set keys for + * @param userId The user id of the user to set the org keys for */ abstract setOrgKeys( orgs: ProfileOrganizationResponse[], providerOrgs: ProfileProviderOrganizationResponse[], + userId: UserId, ): Promise; abstract activeUserOrgKeys$: Observable>; /** @@ -200,7 +212,13 @@ export abstract class CryptoService { * @param providers The providers to set keys for */ abstract activeUserProviderKeys$: Observable>; - abstract setProviderKeys(orgs: ProfileProviderResponse[]): Promise; + + /** + * Stores the provider keys for a given user. + * @param orgs The provider orgs for which to save the keys from. + * @param userId The user id of the user for which to store the keys for. + */ + abstract setProviderKeys(orgs: ProfileProviderResponse[], userId: UserId): Promise; /** * @param providerId The desired provider * @returns The provider's symmetric key @@ -228,7 +246,7 @@ export abstract class CryptoService { * Note: does not clear the private key if null is provided * @param encPrivateKey An encrypted private key */ - abstract setPrivateKey(encPrivateKey: string): Promise; + abstract setPrivateKey(encPrivateKey: string, userId: UserId): Promise; /** * Returns the private key from memory. If not available, decrypts it * from storage and stores it in memory @@ -247,8 +265,9 @@ export abstract class CryptoService { * @param key A key to encrypt the private key with. If not provided, * defaults to the user key * @returns A new keypair: [publicKey in Base64, encrypted privateKey] + * @throws If the provided key is a null-ish value. */ - abstract makeKeyPair(key?: SymmetricCryptoKey): Promise<[string, EncString]>; + abstract makeKeyPair(key: SymmetricCryptoKey): Promise<[string, EncString]>; /** * @param pin The user's pin * @param salt The user's salt diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index 6d9574ef38..873f3ab9a7 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -395,12 +395,11 @@ export class CryptoService implements CryptoServiceAbstraction { } async setOrgKeys( - orgs: ProfileOrganizationResponse[] = [], - providerOrgs: ProfileProviderOrganizationResponse[] = [], + orgs: ProfileOrganizationResponse[], + providerOrgs: ProfileProviderOrganizationResponse[], + userId: UserId, ): Promise { - // 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.activeUserEncryptedOrgKeysState.update((_) => { + await this.stateProvider.getUser(userId, USER_ENCRYPTED_ORGANIZATION_KEYS).update(() => { const encOrgKeyData: { [orgId: string]: EncryptedOrganizationKeyData } = {}; orgs.forEach((org) => { @@ -450,8 +449,8 @@ export class CryptoService implements CryptoServiceAbstraction { await this.stateProvider.setUserState(USER_ENCRYPTED_ORGANIZATION_KEYS, null, userId); } - async setProviderKeys(providers: ProfileProviderResponse[]): Promise { - await this.activeUserEncryptedProviderKeysState.update((_) => { + async setProviderKeys(providers: ProfileProviderResponse[], userId: UserId): Promise { + await this.stateProvider.getUser(userId, USER_ENCRYPTED_PROVIDER_KEYS).update(() => { const encProviderKeys: { [providerId: ProviderId]: EncryptedString } = {}; providers.forEach((provider) => { @@ -494,12 +493,14 @@ export class CryptoService implements CryptoServiceAbstraction { return [encShareKey, shareKey as T]; } - async setPrivateKey(encPrivateKey: EncryptedString): Promise { + async setPrivateKey(encPrivateKey: EncryptedString, userId: UserId): Promise { if (encPrivateKey == null) { return; } - await this.activeUserEncryptedPrivateKeyState.update(() => encPrivateKey); + await this.stateProvider + .getUser(userId, USER_ENCRYPTED_PRIVATE_KEY) + .update(() => encPrivateKey); } async getPrivateKey(): Promise { @@ -523,9 +524,10 @@ export class CryptoService implements CryptoServiceAbstraction { return this.hashPhrase(userFingerprint); } - async makeKeyPair(key?: SymmetricCryptoKey): Promise<[string, EncString]> { - // Default to user key - key ||= await this.getUserKeyWithLegacySupport(); + async makeKeyPair(key: SymmetricCryptoKey): Promise<[string, EncString]> { + if (key == null) { + throw new Error("'key' is a required parameter and must be non-null."); + } const keyPair = await this.cryptoFunctionService.rsaGenerateKeyPair(2048); const publicB64 = Utils.fromBufferToB64(keyPair[0]); diff --git a/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts b/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts index 42ffb5192b..14b26fa541 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts @@ -1,5 +1,5 @@ import { MockProxy, any, mock } from "jest-mock-extended"; -import { BehaviorSubject, of } from "rxjs"; +import { BehaviorSubject, from, of } from "rxjs"; import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; import { SearchService } from "../../abstractions/search.service"; @@ -106,6 +106,13 @@ describe("VaultTimeoutService", () => { // Both are available by default and the specific test can change this per test availableVaultTimeoutActionsSubject.next([VaultTimeoutAction.Lock, VaultTimeoutAction.LogOut]); + authService.authStatusFor$.mockImplementation((userId) => { + return from([ + accounts[userId]?.authStatus ?? AuthenticationStatus.LoggedOut, + AuthenticationStatus.Locked, + ]); + }); + authService.getAuthStatus.mockImplementation((userId) => { return Promise.resolve(accounts[userId]?.authStatus); }); @@ -387,18 +394,6 @@ describe("VaultTimeoutService", () => { expect(stateEventRunnerService.handleEvent).toHaveBeenCalledWith("lock", "user1"); }); - it("should call messaging service locked message if no user passed into lock", async () => { - setupLock(); - - await vaultTimeoutService.lock(); - - // Currently these pass `undefined` (or what they were given) as the userId back - // but we could change this to give the user that was locked (active) to these methods - // so they don't have to get it their own way, but that is a behavioral change that needs - // to be tested. - expect(messagingService.send).toHaveBeenCalledWith("locked", { userId: undefined }); - }); - it("should call locked callback if no user passed into lock", async () => { setupLock(); @@ -414,25 +409,31 @@ describe("VaultTimeoutService", () => { it("should call state event runner with user passed into lock", async () => { setupLock(); - await vaultTimeoutService.lock("user2"); + const user2 = "user2" as UserId; - expect(stateEventRunnerService.handleEvent).toHaveBeenCalledWith("lock", "user2"); + await vaultTimeoutService.lock(user2); + + expect(stateEventRunnerService.handleEvent).toHaveBeenCalledWith("lock", user2); }); it("should call messaging service locked message with user passed into lock", async () => { setupLock(); - await vaultTimeoutService.lock("user2"); + const user2 = "user2" as UserId; - expect(messagingService.send).toHaveBeenCalledWith("locked", { userId: "user2" }); + await vaultTimeoutService.lock(user2); + + expect(messagingService.send).toHaveBeenCalledWith("locked", { userId: user2 }); }); it("should call locked callback with user passed into lock", async () => { setupLock(); - await vaultTimeoutService.lock("user2"); + const user2 = "user2" as UserId; - expect(lockedCallback).toHaveBeenCalledWith("user2"); + await vaultTimeoutService.lock(user2); + + expect(lockedCallback).toHaveBeenCalledWith(user2); }); }); }); diff --git a/libs/common/src/services/vault-timeout/vault-timeout.service.ts b/libs/common/src/services/vault-timeout/vault-timeout.service.ts index 2f3a259562..a75fb6d4c4 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.ts @@ -1,4 +1,4 @@ -import { combineLatest, firstValueFrom, switchMap } from "rxjs"; +import { combineLatest, filter, firstValueFrom, map, switchMap, timeout } from "rxjs"; import { SearchService } from "../../abstractions/search.service"; import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; @@ -80,7 +80,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { ); } - async lock(userId?: string): Promise { + async lock(userId?: UserId): Promise { const authed = await this.stateService.getIsAuthenticated({ userId: userId }); if (!authed) { return; @@ -94,7 +94,27 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { await this.logOut(userId); } - const currentUserId = (await firstValueFrom(this.accountService.activeAccount$)).id; + const currentUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + + const lockingUserId = userId ?? currentUserId; + + // HACK: Start listening for the transition of the locking user from something to the locked state. + // This is very much a hack to ensure that the authentication status to retrievable right after + // it does its work. Particularly the `lockedCallback` and `"locked"` message. Instead + // lockedCallback should be deprecated and people should subscribe and react to `authStatusFor$` themselves. + const lockPromise = firstValueFrom( + this.authService.authStatusFor$(lockingUserId).pipe( + filter((authStatus) => authStatus === AuthenticationStatus.Locked), + timeout({ + first: 5_000, + with: () => { + throw new Error("The lock process did not complete in a reasonable amount of time."); + }, + }), + ), + ); if (userId == null || userId === currentUserId) { await this.searchService.clearIndex(); @@ -102,19 +122,21 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { await this.collectionService.clearActiveUserCache(); } - await this.masterPasswordService.clearMasterKey((userId ?? currentUserId) as UserId); + await this.masterPasswordService.clearMasterKey(lockingUserId); - await this.stateService.setUserKeyAutoUnlock(null, { userId: userId }); - await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId }); + await this.stateService.setUserKeyAutoUnlock(null, { userId: lockingUserId }); + await this.stateService.setCryptoMasterKeyAuto(null, { userId: lockingUserId }); - await this.cipherService.clearCache(userId); + await this.cipherService.clearCache(lockingUserId); - await this.stateEventRunnerService.handleEvent("lock", (userId ?? currentUserId) as UserId); + await this.stateEventRunnerService.handleEvent("lock", lockingUserId); - // FIXME: We should send the userId of the user that was locked, in the case of this method being passed - // undefined then it should give back the currentUserId. Better yet, this method shouldn't take - // an undefined userId at all. All receivers need to be checked for how they handle getting undefined. - this.messagingService.send("locked", { userId: userId }); + // HACK: Sit here and wait for the the auth status to transition to `Locked` + // to ensure the message and lockedCallback will get the correct status + // if/when they call it. + await lockPromise; + + this.messagingService.send("locked", { userId: lockingUserId }); if (this.lockedCallback != null) { await this.lockedCallback(userId); @@ -162,7 +184,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { return diffSeconds >= vaultTimeoutSeconds; } - private async executeTimeoutAction(userId: string): Promise { + private async executeTimeoutAction(userId: UserId): Promise { const timeoutAction = await firstValueFrom( this.vaultTimeoutSettingsService.vaultTimeoutAction$(userId), ); diff --git a/libs/common/src/vault/services/sync/sync.service.ts b/libs/common/src/vault/services/sync/sync.service.ts index 995ab7319b..793bcf2437 100644 --- a/libs/common/src/vault/services/sync/sync.service.ts +++ b/libs/common/src/vault/services/sync/sync.service.ts @@ -35,7 +35,6 @@ import { SendData } from "../../../tools/send/models/data/send.data"; import { SendResponse } from "../../../tools/send/models/response/send.response"; import { SendApiService } from "../../../tools/send/services/send-api.service.abstraction"; import { InternalSendService } from "../../../tools/send/services/send.service.abstraction"; -import { UserId } from "../../../types/guid"; import { CipherService } from "../../../vault/abstractions/cipher.service"; import { FolderApiServiceAbstraction } from "../../../vault/abstractions/folder/folder-api.service.abstraction"; import { InternalFolderService } from "../../../vault/abstractions/folder/folder.service.abstraction"; @@ -311,7 +310,7 @@ export class SyncService implements SyncServiceAbstraction { } private async syncProfile(response: ProfileResponse) { - const stamp = await this.tokenService.getSecurityStamp(response.id as UserId); + const stamp = await this.tokenService.getSecurityStamp(response.id); if (stamp != null && stamp !== response.securityStamp) { if (this.logoutCallback != null) { await this.logoutCallback(true); @@ -321,15 +320,16 @@ export class SyncService implements SyncServiceAbstraction { } await this.cryptoService.setMasterKeyEncryptedUserKey(response.key); - await this.cryptoService.setPrivateKey(response.privateKey); - await this.cryptoService.setProviderKeys(response.providers); - await this.cryptoService.setOrgKeys(response.organizations, response.providerOrganizations); - await this.avatarService.setSyncAvatarColor(response.id as UserId, response.avatarColor); - await this.tokenService.setSecurityStamp(response.securityStamp, response.id as UserId); - await this.accountService.setAccountEmailVerified( - response.id as UserId, - response.emailVerified, + await this.cryptoService.setPrivateKey(response.privateKey, response.id); + await this.cryptoService.setProviderKeys(response.providers, response.id); + await this.cryptoService.setOrgKeys( + response.organizations, + response.providerOrganizations, + response.id, ); + await this.avatarService.setSyncAvatarColor(response.id, response.avatarColor); + await this.tokenService.setSecurityStamp(response.securityStamp, response.id); + await this.accountService.setAccountEmailVerified(response.id, response.emailVerified); await this.billingAccountProfileStateService.setHasPremium( response.premiumPersonally, diff --git a/libs/components/src/item/item-content.component.html b/libs/components/src/item/item-content.component.html index d034a4a001..da69c79c1e 100644 --- a/libs/components/src/item/item-content.component.html +++ b/libs/components/src/item/item-content.component.html @@ -1,11 +1,11 @@ -
+
-
+
-
+
diff --git a/libs/components/src/item/item.component.html b/libs/components/src/item/item.component.html index 0c91c6848e..c02117058f 100644 --- a/libs/components/src/item/item.component.html +++ b/libs/components/src/item/item.component.html @@ -7,7 +7,7 @@ : 'tw-border-b-secondary-300 [&:has(.item-main-content_button:hover,.item-main-content_a:hover)]:tw-border-b-transparent' " > - + diff --git a/libs/components/src/item/item.stories.ts b/libs/components/src/item/item.stories.ts index b9d8d6cc2e..5198d7efa6 100644 --- a/libs/components/src/item/item.stories.ts +++ b/libs/components/src/item/item.stories.ts @@ -113,11 +113,19 @@ export const TextOverflow: Story = { render: (args) => ({ props: args, template: /*html*/ ` -
TODO: Fix truncation
- Helloooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo + Helloooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo! + Worlddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd! + + + + + + + + `, }),