diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 2a7f18ca0139..a95f11ffa1e9 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 e1199857c72e..4ffbb147be84 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 c167d890063e..6ffbf522d348 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/auth/background/service-factories/device-trust-service.factory.ts b/apps/browser/src/auth/background/service-factories/device-trust-service.factory.ts index 106bcbcf72d6..42a8232c3e61 100644 --- a/apps/browser/src/auth/background/service-factories/device-trust-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/device-trust-service.factory.ts @@ -34,6 +34,7 @@ import { KeyGenerationServiceInitOptions, keyGenerationServiceFactory, } from "../../../platform/background/service-factories/key-generation-service.factory"; +import { logServiceFactory } from "../../../platform/background/service-factories/log-service.factory"; import { PlatformUtilsServiceInitOptions, platformUtilsServiceFactory, @@ -88,6 +89,7 @@ export function deviceTrustServiceFactory( await stateProviderFactory(cache, opts), await secureStorageServiceFactory(cache, opts), await userDecryptionOptionsServiceFactory(cache, opts), + await logServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 2bdb93bd07a8..5cd4113bae5d 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, @@ -631,6 +631,7 @@ export default class MainBackground { this.stateProvider, this.secureStorageService, this.userDecryptionOptionsService, + this.logService, ); this.devicesService = new DevicesServiceImplementation(this.devicesApiService); @@ -1200,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; - - await this.eventUploadService.uploadEvents(userId as UserId); + 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"); + }, + }), + ), + ); + + 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 @@ -1235,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.stateService.clean({ userId: userBeingLoggedOut }); + await this.accountService.clean(userBeingLoggedOut); + + await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut); - await this.stateEventRunnerService.handleEvent("logout", userId); + // 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/src/platform/popup/layout/popup-tab-navigation.component.ts b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts index 3a275454d943..ced3f6462e98 100644 --- a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts +++ b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts @@ -17,25 +17,25 @@ export class PopupTabNavigationComponent { navButtons = [ { label: "Vault", - page: "/vault", + page: "/tabs/vault", iconKey: "lock", iconKeyActive: "lock-f", }, { label: "Generator", - page: "/generator", + page: "/tabs/generator", iconKey: "generate", iconKeyActive: "generate-f", }, { label: "Send", - page: "/send", + page: "/tabs/send", iconKey: "send", iconKeyActive: "send-f", }, { label: "Settings", - page: "/settings", + page: "/tabs/settings", iconKey: "cog", iconKeyActive: "cog-f", }, diff --git a/apps/browser/src/platform/services/default-browser-state.service.ts b/apps/browser/src/platform/services/default-browser-state.service.ts index b0b9e3748f83..d7bc45bcc374 100644 --- a/apps/browser/src/platform/services/default-browser-state.service.ts +++ b/apps/browser/src/platform/services/default-browser-state.service.ts @@ -1,5 +1,3 @@ -import { BehaviorSubject } from "rxjs"; - import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -15,21 +13,13 @@ import { MigrationRunner } from "@bitwarden/common/platform/services/migration-r import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service"; import { Account } from "../../models/account"; -import { browserSession, sessionSync } from "../decorators/session-sync-observable"; import { BrowserStateService } from "./abstractions/browser-state.service"; -@browserSession export class DefaultBrowserStateService extends BaseStateService implements BrowserStateService { - @sessionSync({ - initializer: Account.fromJSON as any, // TODO: Remove this any when all any types are removed from Account - initializeAs: "record", - }) - protected accountsSubject: BehaviorSubject<{ [userId: string]: Account }>; - protected accountDeserializer = Account.fromJSON; constructor( diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 14659cb4dfbb..0dcf496457a5 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -2,9 +2,9 @@ import { Injectable, NgModule } from "@angular/core"; import { ActivatedRouteSnapshot, RouteReuseStrategy, RouterModule, Routes } from "@angular/router"; import { - redirectGuard, AuthGuard, lockGuard, + redirectGuard, tdeDecryptionRequiredGuard, unauthGuardFn, } from "@bitwarden/angular/auth/guards"; @@ -47,6 +47,7 @@ import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items import { ViewComponent } from "../vault/popup/components/vault/view.component"; import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component"; +import { extensionRefreshRedirect, extensionRefreshSwap } from "./extension-refresh-route-utils"; import { debounceNavigationGuard } from "./services/debounce-navigation.service"; import { ExcludedDomainsComponent } from "./settings/excluded-domains.component"; import { FoldersComponent } from "./settings/folders.component"; @@ -54,6 +55,7 @@ import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component import { OptionsComponent } from "./settings/options.component"; import { SettingsComponent } from "./settings/settings.component"; import { SyncComponent } from "./settings/sync.component"; +import { TabsV2Component } from "./tabs-v2.component"; import { TabsComponent } from "./tabs.component"; const unauthRouteOverrides = { @@ -322,9 +324,8 @@ const routes: Routes = [ canActivate: [AuthGuard], data: { state: "help-and-feedback" }, }, - { + ...extensionRefreshSwap(TabsComponent, TabsV2Component, { path: "tabs", - component: TabsComponent, data: { state: "tabs" }, children: [ { @@ -336,6 +337,7 @@ const routes: Routes = [ path: "current", component: CurrentTabComponent, canActivate: [AuthGuard], + canMatch: [extensionRefreshRedirect("/tabs/vault")], data: { state: "tabs_current" }, runGuardsAndResolvers: "always", }, @@ -364,7 +366,7 @@ const routes: Routes = [ data: { state: "tabs_send" }, }, ], - }, + }), { path: "account-switcher", component: AccountSwitcherComponent, diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index a6e953ad1dc8..bed40dfddc9c 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -80,6 +80,7 @@ import { OptionsComponent } from "./settings/options.component"; import { SettingsComponent } from "./settings/settings.component"; import { SyncComponent } from "./settings/sync.component"; import { VaultTimeoutInputComponent } from "./settings/vault-timeout-input.component"; +import { TabsV2Component } from "./tabs-v2.component"; import { TabsComponent } from "./tabs.component"; // Register the locales for the application @@ -160,6 +161,7 @@ import "../platform/popup/locales"; SsoComponent, SyncComponent, TabsComponent, + TabsV2Component, TwoFactorComponent, TwoFactorOptionsComponent, UpdateTempPasswordComponent, diff --git a/apps/browser/src/popup/extension-refresh-route-utils.ts b/apps/browser/src/popup/extension-refresh-route-utils.ts new file mode 100644 index 000000000000..3c2ca33f86e7 --- /dev/null +++ b/apps/browser/src/popup/extension-refresh-route-utils.ts @@ -0,0 +1,45 @@ +import { inject, Type } from "@angular/core"; +import { Route, Router, Routes, UrlTree } from "@angular/router"; + +import { componentRouteSwap } from "@bitwarden/angular/utils/component-route-swap"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; + +/** + * Helper function to swap between two components based on the ExtensionRefresh feature flag. + * @param defaultComponent - The current non-refreshed component to render. + * @param refreshedComponent - The new refreshed component to render. + * @param options - The shared route options to apply to both components. + */ +export function extensionRefreshSwap( + defaultComponent: Type, + refreshedComponent: Type, + options: Route, +): Routes { + return componentRouteSwap( + defaultComponent, + refreshedComponent, + async () => { + const configService = inject(ConfigService); + return configService.getFeatureFlag(FeatureFlag.ExtensionRefresh); + }, + options, + ); +} + +/** + * Helper function to redirect to a new URL based on the ExtensionRefresh feature flag. + * @param redirectUrl - The URL to redirect to if the ExtensionRefresh flag is enabled. + */ +export function extensionRefreshRedirect(redirectUrl: string): () => Promise { + return async () => { + const configService = inject(ConfigService); + const router = inject(Router); + const shouldRedirect = await configService.getFeatureFlag(FeatureFlag.ExtensionRefresh); + if (shouldRedirect) { + return router.parseUrl(redirectUrl); + } else { + return true; + } + }; +} diff --git a/apps/browser/src/popup/tabs-v2.component.ts b/apps/browser/src/popup/tabs-v2.component.ts new file mode 100644 index 000000000000..4cdb8fc029d5 --- /dev/null +++ b/apps/browser/src/popup/tabs-v2.component.ts @@ -0,0 +1,11 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-tabs-v2", + template: ` + + + + `, +}) +export class TabsV2Component {} diff --git a/apps/browser/store/locales/en_GB/copy.resx b/apps/browser/store/locales/en_GB/copy.resx index 82e4eb1d88ef..7c408ad88976 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 82e4eb1d88ef..31e5d2326f60 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 472697d825cc..019006422a98 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 34461983bc81..65b81e2d2d40 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/cli/src/bw.ts b/apps/cli/src/bw.ts index 114765a789ac..a038f3aa904c 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -486,6 +486,7 @@ export class Main { this.stateProvider, this.secureStorageService, this.userDecryptionOptionsService, + this.logService, ); this.authRequestService = new AuthRequestService( diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 4e540efdc66d..056fb3f51e64 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"; @@ -218,8 +218,10 @@ export class AppComponent implements OnInit, OnDestroy { await this.vaultTimeoutService.lock(message.userId); break; case "lockAllVaults": { - const currentUser = await this.stateService.getUserId(); - const accounts = await firstValueFrom(this.stateService.accounts$); + const currentUser = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a.id)), + ); + const accounts = await firstValueFrom(this.accountService.accounts$); await this.vaultTimeoutService.lock(currentUser); for (const account of Object.keys(accounts)) { if (account === currentUser) { @@ -564,19 +566,42 @@ export class AppComponent implements OnInit, OnDestroy { this.messagingService.send("updateAppMenu", { updateRequest: updateRequest }); } - private async logOut(expired: boolean, userId?: string) { - const userBeingLoggedOut = - (userId as UserId) ?? - (await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id)))); + // 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) { + 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); @@ -590,26 +615,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( @@ -690,7 +722,7 @@ export class AppComponent implements OnInit, OnDestroy { } private async checkForSystemTimeout(timeout: number): Promise { - const accounts = await firstValueFrom(this.stateService.accounts$); + const accounts = await firstValueFrom(this.accountService.accounts$); for (const userId in accounts) { if (userId == null) { continue; @@ -700,7 +732,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 c8a26065c11b..92cfebfd605a 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/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index a485b925ba6d..2acf6dde5a52 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -221,7 +221,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: EncryptedMessageHandlerService, deps: [ - StateServiceAbstraction, + AccountServiceAbstraction, AuthServiceAbstraction, CipherServiceAbstraction, PolicyServiceAbstraction, diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index 5c8c32b7c143..5572d2fd352b 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 abfa0b1c0dac..79f2de8f63c9 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/desktop/src/services/encrypted-message-handler.service.ts b/apps/desktop/src/services/encrypted-message-handler.service.ts index e38339d5ad07..4512e175cebb 100644 --- a/apps/desktop/src/services/encrypted-message-handler.service.ts +++ b/apps/desktop/src/services/encrypted-message-handler.service.ts @@ -1,12 +1,13 @@ -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -28,7 +29,7 @@ import { UserStatusErrorResponse } from "../models/native-messaging/encrypted-me export class EncryptedMessageHandlerService { constructor( - private stateService: StateService, + private accountService: AccountService, private authService: AuthService, private cipherService: CipherService, private policyService: PolicyService, @@ -62,7 +63,9 @@ export class EncryptedMessageHandlerService { } private async checkUserStatus(userId: string): Promise { - const activeUserId = await this.stateService.getUserId(); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); if (userId !== activeUserId) { return "not-active-user"; @@ -77,17 +80,19 @@ export class EncryptedMessageHandlerService { } private async statusCommandHandler(): Promise { - const accounts = await firstValueFrom(this.stateService.accounts$); - const activeUserId = await this.stateService.getUserId(); + const accounts = await firstValueFrom(this.accountService.accounts$); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); if (!accounts || !Object.keys(accounts)) { return []; } return Promise.all( - Object.keys(accounts).map(async (userId) => { + Object.keys(accounts).map(async (userId: UserId) => { const authStatus = await this.authService.getAuthStatus(userId); - const email = await this.stateService.getEmail({ userId }); + const email = accounts[userId].email; return { id: userId, @@ -107,7 +112,9 @@ export class EncryptedMessageHandlerService { } const ciphersResponse: CipherResponse[] = []; - const activeUserId = await this.stateService.getUserId(); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); const authStatus = await this.authService.getAuthStatus(activeUserId); if (authStatus !== AuthenticationStatus.Unlocked) { diff --git a/apps/desktop/src/services/native-messaging.service.ts b/apps/desktop/src/services/native-messaging.service.ts index 01d947697775..48bdc6004769 100644 --- a/apps/desktop/src/services/native-messaging.service.ts +++ b/apps/desktop/src/services/native-messaging.service.ts @@ -1,6 +1,7 @@ import { Injectable, NgZone } from "@angular/core"; import { firstValueFrom } from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { MasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -41,6 +42,7 @@ export class NativeMessagingService { private biometricStateService: BiometricStateService, private nativeMessageHandler: NativeMessageHandlerService, private dialogService: DialogService, + private accountService: AccountService, private ngZone: NgZone, ) {} @@ -51,9 +53,7 @@ export class NativeMessagingService { private async messageHandler(msg: LegacyMessageWrapper | Message) { const outerMessage = msg as Message; if (outerMessage.version) { - // 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.nativeMessageHandler.handleMessage(outerMessage); + await this.nativeMessageHandler.handleMessage(outerMessage); return; } @@ -64,7 +64,7 @@ export class NativeMessagingService { const remotePublicKey = Utils.fromB64ToArray(rawMessage.publicKey); // Validate the UserId to ensure we are logged into the same account. - const accounts = await firstValueFrom(this.stateService.accounts$); + const accounts = await firstValueFrom(this.accountService.accounts$); const userIds = Object.keys(accounts); if (!userIds.includes(rawMessage.userId)) { ipc.platform.nativeMessaging.sendMessage({ @@ -81,7 +81,7 @@ export class NativeMessagingService { }); const fingerprint = await this.cryptoService.getFingerprint( - await this.stateService.getUserId(), + rawMessage.userId, remotePublicKey, ); @@ -98,9 +98,7 @@ export class NativeMessagingService { } } - // 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.secureCommunication(remotePublicKey, appId); + await this.secureCommunication(remotePublicKey, appId); return; } @@ -144,9 +142,7 @@ export class NativeMessagingService { ? firstValueFrom(this.biometricStateService.biometricUnlockEnabled$) : this.biometricStateService.getBiometricUnlockEnabled(message.userId as UserId); if (!(await biometricUnlockPromise)) { - // 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.send({ command: "biometricUnlock", response: "not enabled" }, appId); + await this.send({ command: "biometricUnlock", response: "not enabled" }, appId); return this.ngZone.run(() => this.dialogService.openSimpleDialog({ @@ -172,9 +168,7 @@ export class NativeMessagingService { // we send the master key still for backwards compatibility // with older browser extensions // TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3472) - // 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.send( + await this.send( { command: "biometricUnlock", response: "unlocked", @@ -184,14 +178,10 @@ export class NativeMessagingService { appId, ); } else { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.send({ command: "biometricUnlock", response: "canceled" }, appId); + await this.send({ command: "biometricUnlock", response: "canceled" }, appId); } } catch (e) { - // 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.send({ command: "biometricUnlock", response: "canceled" }, appId); + await this.send({ command: "biometricUnlock", response: "canceled" }, appId); } break; 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 80d77968f2d2..8cf7ed313feb 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 8584e1a00f32..7d2b6d999aa9 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -3,13 +3,14 @@ import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core"; import { NavigationEnd, Router } from "@angular/router"; import * as jq from "jquery"; import { + Subject, combineLatest, filter, firstValueFrom, map, - Subject, switchMap, takeUntil, + timeout, timer, } from "rxjs"; @@ -22,6 +23,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 { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; @@ -146,9 +148,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(); @@ -280,7 +280,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(), @@ -288,11 +301,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 () => { @@ -305,6 +318,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/app/auth/emergency-access/services/emergency-access.service.spec.ts b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts index 1d54726727b1..6de3fd9d8b4d 100644 --- a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts +++ b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts @@ -153,6 +153,7 @@ describe("EmergencyAccessService", () => { } as EmergencyAccessTakeoverResponse); const mockDecryptedGrantorUserKey = new Uint8Array(64); + cryptoService.getPrivateKey.mockResolvedValue(new Uint8Array(64)); cryptoService.rsaDecrypt.mockResolvedValueOnce(mockDecryptedGrantorUserKey); const mockMasterKey = new SymmetricCryptoKey(new Uint8Array(64) as CsprngArray) as MasterKey; @@ -197,6 +198,7 @@ describe("EmergencyAccessService", () => { kdf: KdfType.PBKDF2_SHA256, kdfIterations: 500, } as EmergencyAccessTakeoverResponse); + cryptoService.getPrivateKey.mockResolvedValue(new Uint8Array(64)); await expect( emergencyAccessService.takeover(mockId, mockEmail, mockName), @@ -204,6 +206,21 @@ describe("EmergencyAccessService", () => { expect(emergencyAccessApiService.postEmergencyAccessPassword).not.toHaveBeenCalled(); }); + + it("should throw an error if the users private key cannot be retrieved", async () => { + emergencyAccessApiService.postEmergencyAccessTakeover.mockResolvedValueOnce({ + keyEncrypted: "EncryptedKey", + kdf: KdfType.PBKDF2_SHA256, + kdfIterations: 500, + } as EmergencyAccessTakeoverResponse); + cryptoService.getPrivateKey.mockResolvedValue(null); + + await expect(emergencyAccessService.takeover(mockId, mockEmail, mockName)).rejects.toThrow( + "user does not have a private key", + ); + + expect(emergencyAccessApiService.postEmergencyAccessPassword).not.toHaveBeenCalled(); + }); }); describe("getRotatedKeys", () => { diff --git a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts index dbc1ce820c61..819b80c1ad73 100644 --- a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts +++ b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts @@ -209,7 +209,16 @@ export class EmergencyAccessService { async getViewOnlyCiphers(id: string): Promise { const response = await this.emergencyAccessApiService.postEmergencyAccessView(id); - const grantorKeyBuffer = await this.cryptoService.rsaDecrypt(response.keyEncrypted); + const activeUserPrivateKey = await this.cryptoService.getPrivateKey(); + + if (activeUserPrivateKey == null) { + throw new Error("Active user does not have a private key, cannot get view only ciphers."); + } + + const grantorKeyBuffer = await this.cryptoService.rsaDecrypt( + response.keyEncrypted, + activeUserPrivateKey, + ); const grantorUserKey = new SymmetricCryptoKey(grantorKeyBuffer) as UserKey; const ciphers = await this.encryptService.decryptItems( @@ -229,7 +238,16 @@ export class EmergencyAccessService { async takeover(id: string, masterPassword: string, email: string) { const takeoverResponse = await this.emergencyAccessApiService.postEmergencyAccessTakeover(id); - const grantorKeyBuffer = await this.cryptoService.rsaDecrypt(takeoverResponse.keyEncrypted); + const activeUserPrivateKey = await this.cryptoService.getPrivateKey(); + + if (activeUserPrivateKey == null) { + throw new Error("Active user does not have a private key, cannot complete a takeover."); + } + + const grantorKeyBuffer = await this.cryptoService.rsaDecrypt( + takeoverResponse.keyEncrypted, + activeUserPrivateKey, + ); if (grantorKeyBuffer == null) { throw new Error("Failed to decrypt grantor key"); } diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index 2c05de9954a1..fd873a41f1bc 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 9353797bb452..bb9998268474 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 19e38577a453..795b6bee8e65 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 de563800eab8..ba424f456b79 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 f5465fb658f6..5b071f66585b 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 1b21a9a04d79..0de85f1b9f96 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 2382a31caf0e..de846aa80177 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 80fd0802d330..443348f1e075 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 32f1f68d4574..ae9df7bb1d6d 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 09cc963d6746..791f940f111d 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 30ba2d09e45e..f4b6559cfcd1 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 1f520f546d49..f7d0e08273a3 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 b57f8728e58e..6dab7f4816b5 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 f5fff9a81dd8..4840003abdfe 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 6910230fc403..bf746dd0d5a9 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 14d1b49c8394..3343910c8a75 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 51374f843208..4f9ffd2f0f9d 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 d49dbb60000a..338d172bb72c 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 ef4d3f8ad38d..d2ea371f43ae 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 9509214f0efe..9b208f945a0a 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 d8f2fd947a49..45a32b201101 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 17b629efd528..eb0e93114b36 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 aed2665fcc76..e3806b65413c 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 bd0144d7a59d..eac5e78d8793 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 2d7c32595cc9..417c7adc6ade 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 e5a57df2c36e..c448a25e59bb 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 5439aee5e530..5e0d3b8c29ab 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 ed5fb336156f..6eac81da086d 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 ca5fa9773620..1c99680f6254 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 dae66593f935..0aacc90be7fb 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 9a553179781b..5c3894d4431a 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 b93dbe7a6059..8bf56e6f657d 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 c05eab531431..104810e93658 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 2d7c32595cc9..417c7adc6ade 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 a7f71b2ef986..3b34cc79767d 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 86695a392af4..76b86a74334a 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 bf47a1009d94..cd5991ce7cc0 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 d3a958a95b88..3bd5ca30495d 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 2d7c32595cc9..417c7adc6ade 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 2d7c32595cc9..417c7adc6ade 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 adb0dc144dc2..5b544ff3a4d8 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 6590b35547c1..24aaf3d0d372 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 17d396386c48..5a203b5a5117 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 35c43669a4c7..7569f5a4ee93 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 2d7c32595cc9..417c7adc6ade 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 66aacde73262..e1a48f0fb00b 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 7c94396334c4..fc3c4b1a5190 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 38bc977d3ad3..5446a9dd65de 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 1d54a578d598..eb40a84da989 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 bc1ff8d6eb36..92eced8799e6 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 f74146210eec..f264457250f2 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 c47022d7856b..7dd89c23be31 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 8f8102d14e22..5a85e934f043 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 1b060d0d3fa4..38bc66f911a7 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 d4d435759a52..ecc90937488a 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 427ea5f9c353..1d082c5eabe7 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 2d7c32595cc9..417c7adc6ade 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 091660dd834c..dd519daa296b 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 b439f60538ff..2954942728d2 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 9dcb10f10196..23aa9b94b901 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 43718cd3475d..aa5ad2c9d358 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 0631291823e8..8e5a9e696b72 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 5e4d8f5d486e..c2b0060ac699 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/clients/clients.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts index 6875c3816b06..7a96bdc7c706 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts @@ -1,17 +1,17 @@ import { Component } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; -import { combineLatest, firstValueFrom, from } from "rxjs"; -import { concatMap, switchMap, takeUntil } from "rxjs/operators"; +import { firstValueFrom, from, map } from "rxjs"; +import { switchMap, takeUntil } from "rxjs/operators"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; -import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums"; +import { ProviderUserType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { canAccessBilling } from "@bitwarden/common/billing/abstractions/provider-billing.service.abstraction"; import { PlanType } from "@bitwarden/common/billing/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; @@ -40,10 +40,6 @@ export class ClientsComponent extends BaseClientsComponent { manageOrganizations = false; showAddExisting = false; - protected consolidatedBillingEnabled$ = this.configService.getFeatureFlag$( - FeatureFlag.EnableConsolidatedBilling, - ); - constructor( private router: Router, private providerService: ProviderService, @@ -75,15 +71,10 @@ export class ClientsComponent extends BaseClientsComponent { .pipe( switchMap((params) => { this.providerId = params.providerId; - return combineLatest([ - this.providerService.get(this.providerId), - this.consolidatedBillingEnabled$, - ]).pipe( - concatMap(([provider, consolidatedBillingEnabled]) => { - if ( - consolidatedBillingEnabled && - provider.providerStatus === ProviderStatusType.Billable - ) { + return this.providerService.get$(this.providerId).pipe( + canAccessBilling(this.configService), + map((canAccessBilling) => { + if (canAccessBilling) { return from( this.router.navigate(["../manage-client-organizations"], { relativeTo: this.activatedRoute, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html index 55efbe138649..a1cf2cc5aaba 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html @@ -1,5 +1,5 @@ -