From 5006a2954611374a44c00481a5748e0e929e95a9 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Fri, 11 Apr 2025 09:12:18 -0700 Subject: [PATCH 01/47] [PM-17516][PM-17617] - Remove old add-edit and attachments components (#14087)ew * remove unused components * re-add add-edit * re-delete add-edit --- .../src/app/shared/loose-components.module.ts | 12 - .../individual-vault/add-edit.component.html | 1112 ----------------- .../individual-vault/add-edit.component.ts | 315 ----- .../attachments.component.html | 120 -- .../individual-vault/attachments.component.ts | 67 - .../app/vault/org-vault/add-edit.component.ts | 138 -- .../vault/org-vault/attachments.component.ts | 105 -- 7 files changed, 1869 deletions(-) delete mode 100644 apps/web/src/app/vault/individual-vault/add-edit.component.html delete mode 100644 apps/web/src/app/vault/individual-vault/add-edit.component.ts delete mode 100644 apps/web/src/app/vault/individual-vault/attachments.component.html delete mode 100644 apps/web/src/app/vault/individual-vault/attachments.component.ts delete mode 100644 apps/web/src/app/vault/org-vault/add-edit.component.ts delete mode 100644 apps/web/src/app/vault/org-vault/attachments.component.ts diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index 5de56789cad..5dc34b3b5b1 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -57,13 +57,9 @@ import { WeakPasswordsReportComponent as OrgWeakPasswordsReportComponent } from /* eslint no-restricted-imports: "error" */ import { PremiumBadgeComponent } from "../vault/components/premium-badge.component"; import { AddEditCustomFieldsComponent } from "../vault/individual-vault/add-edit-custom-fields.component"; -import { AddEditComponent } from "../vault/individual-vault/add-edit.component"; -import { AttachmentsComponent } from "../vault/individual-vault/attachments.component"; import { FolderAddEditComponent } from "../vault/individual-vault/folder-add-edit.component"; import { OrganizationBadgeModule } from "../vault/individual-vault/organization-badge/organization-badge.module"; import { PipesModule } from "../vault/individual-vault/pipes/pipes.module"; -import { AddEditComponent as OrgAddEditComponent } from "../vault/org-vault/add-edit.component"; -import { AttachmentsComponent as OrgAttachmentsComponent } from "../vault/org-vault/attachments.component"; import { PurgeVaultComponent } from "../vault/settings/purge-vault.component"; import { EnvironmentSelectorModule } from "./../components/environment-selector/environment-selector.module"; @@ -97,11 +93,9 @@ import { SharedModule } from "./shared.module"; declarations: [ AcceptFamilySponsorshipComponent, AccountComponent, - AddEditComponent, AddEditCustomFieldsComponent, AddEditCustomFieldsComponent, ApiKeyComponent, - AttachmentsComponent, ChangeEmailComponent, DeauthorizeSessionsComponent, DeleteAccountDialogComponent, @@ -113,8 +107,6 @@ import { SharedModule } from "./shared.module"; EmergencyAccessViewComponent, FolderAddEditComponent, FrontendLayoutComponent, - OrgAddEditComponent, - OrgAttachmentsComponent, OrgEventsComponent, OrgExposedPasswordsReportComponent, OrgInactiveTwoFactorReportComponent, @@ -146,11 +138,9 @@ import { SharedModule } from "./shared.module"; UserVerificationModule, PremiumBadgeComponent, AccountComponent, - AddEditComponent, AddEditCustomFieldsComponent, AddEditCustomFieldsComponent, ApiKeyComponent, - AttachmentsComponent, ChangeEmailComponent, DeauthorizeSessionsComponent, DeleteAccountDialogComponent, @@ -163,9 +153,7 @@ import { SharedModule } from "./shared.module"; EmergencyAccessViewComponent, FolderAddEditComponent, FrontendLayoutComponent, - OrgAddEditComponent, OrganizationLayoutComponent, - OrgAttachmentsComponent, OrgEventsComponent, OrgExposedPasswordsReportComponent, OrgInactiveTwoFactorReportComponent, diff --git a/apps/web/src/app/vault/individual-vault/add-edit.component.html b/apps/web/src/app/vault/individual-vault/add-edit.component.html deleted file mode 100644 index 29de65efcd5..00000000000 --- a/apps/web/src/app/vault/individual-vault/add-edit.component.html +++ /dev/null @@ -1,1112 +0,0 @@ - diff --git a/apps/web/src/app/vault/individual-vault/add-edit.component.ts b/apps/web/src/app/vault/individual-vault/add-edit.component.ts deleted file mode 100644 index d8739465938..00000000000 --- a/apps/web/src/app/vault/individual-vault/add-edit.component.ts +++ /dev/null @@ -1,315 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { DatePipe } from "@angular/common"; -import { Component, OnDestroy, OnInit } from "@angular/core"; -import { firstValueFrom, map } from "rxjs"; - -import { CollectionService } from "@bitwarden/admin-console/common"; -import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/vault/components/add-edit.component"; -import { AuditService } from "@bitwarden/common/abstractions/audit.service"; -import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { isCardExpired } from "@bitwarden/common/autofill/utils"; -import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { ProductTierType } from "@bitwarden/common/billing/enums"; -import { EventType } from "@bitwarden/common/enums"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; -import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; -import { CipherType } from "@bitwarden/common/vault/enums"; -import { Launchable } from "@bitwarden/common/vault/interfaces/launchable"; -import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; -import { DialogService, ToastService } from "@bitwarden/components"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; -import { PasswordRepromptService, SshImportPromptService } from "@bitwarden/vault"; - -@Component({ - selector: "app-vault-add-edit", - templateUrl: "add-edit.component.html", -}) -export class AddEditComponent extends BaseAddEditComponent implements OnInit, OnDestroy { - canAccessPremium: boolean; - totpCode: string; - totpCodeFormatted: string; - totpDash: number; - totpSec: number; - totpLow: boolean; - showRevisionDate = false; - hasPasswordHistory = false; - viewingPasswordHistory = false; - viewOnly = false; - showPasswordCount = false; - cardIsExpired: boolean = false; - - protected totpInterval: number; - protected override componentName = "app-vault-add-edit"; - - constructor( - cipherService: CipherService, - folderService: FolderService, - i18nService: I18nService, - platformUtilsService: PlatformUtilsService, - auditService: AuditService, - accountService: AccountService, - collectionService: CollectionService, - protected totpService: TotpService, - protected passwordGenerationService: PasswordGenerationServiceAbstraction, - protected messagingService: MessagingService, - eventCollectionService: EventCollectionService, - protected policyService: PolicyService, - organizationService: OrganizationService, - logService: LogService, - passwordRepromptService: PasswordRepromptService, - dialogService: DialogService, - datePipe: DatePipe, - configService: ConfigService, - private billingAccountProfileStateService: BillingAccountProfileStateService, - cipherAuthorizationService: CipherAuthorizationService, - toastService: ToastService, - sdkService: SdkService, - sshImportPromptService: SshImportPromptService, - ) { - super( - cipherService, - folderService, - i18nService, - platformUtilsService, - auditService, - accountService, - collectionService, - messagingService, - eventCollectionService, - policyService, - logService, - passwordRepromptService, - organizationService, - dialogService, - window, - datePipe, - configService, - cipherAuthorizationService, - toastService, - sdkService, - sshImportPromptService, - ); - } - - async ngOnInit() { - await super.ngOnInit(); - await this.load(); - - this.viewOnly = !this.cipher.edit && this.editMode; - // remove when all the title for all clients are updated to New Item - if (this.cloneMode || !this.editMode) { - this.title = this.i18nService.t("newItem"); - } - this.showRevisionDate = this.cipher.passwordRevisionDisplayDate != null; - this.hasPasswordHistory = this.cipher.hasPasswordHistory; - this.cleanUp(); - - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a.id)), - ); - - this.canAccessPremium = await firstValueFrom( - this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId), - ); - - if (this.showTotp()) { - await this.totpUpdateCode(); - const totpResponse = await firstValueFrom(this.totpService.getCode$(this.cipher.login.totp)); - if (totpResponse) { - const interval = totpResponse.period; - await this.totpTick(interval); - - this.totpInterval = window.setInterval(async () => { - await this.totpTick(interval); - }, 1000); - } - } - - this.cardIsExpired = isCardExpired(this.cipher.card); - } - - ngOnDestroy() { - super.ngOnDestroy(); - } - - toggleFavorite() { - this.cipher.favorite = !this.cipher.favorite; - } - - togglePassword() { - super.togglePassword(); - - // Hide password count when password is hidden to be safe - if (!this.showPassword && this.showPasswordCount) { - this.togglePasswordCount(); - } - } - - togglePasswordCount() { - this.showPasswordCount = !this.showPasswordCount; - - if (this.editMode && this.showPasswordCount) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.eventCollectionService.collect( - EventType.Cipher_ClientToggledPasswordVisible, - this.cipherId, - ); - } - } - - launch(uri: Launchable) { - if (!uri.canLaunch) { - return; - } - - this.platformUtilsService.launchUri(uri.launchUri); - } - - async copy(value: string, typeI18nKey: string, aType: string): Promise { - if (value == null) { - return false; - } - - this.platformUtilsService.copyToClipboard(value, { window: window }); - this.toastService.showToast({ - variant: "info", - title: null, - message: this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)), - }); - - if (this.editMode) { - if (typeI18nKey === "password") { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.eventCollectionService.collect(EventType.Cipher_ClientCopiedPassword, this.cipherId); - } else if (typeI18nKey === "securityCode") { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.eventCollectionService.collect(EventType.Cipher_ClientCopiedCardCode, this.cipherId); - } else if (aType === "H_Field") { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.eventCollectionService.collect( - EventType.Cipher_ClientCopiedHiddenField, - this.cipherId, - ); - } - } - - return true; - } - - async generatePassword(): Promise { - const confirmed = await super.generatePassword(); - if (confirmed) { - const options = (await this.passwordGenerationService.getOptions())?.[0] ?? {}; - this.cipher.login.password = await this.passwordGenerationService.generatePassword(options); - } - return confirmed; - } - - premiumRequired() { - if (!this.canAccessPremium) { - this.messagingService.send("premiumRequired"); - return; - } - } - - upgradeOrganization() { - this.messagingService.send("upgradeOrganization", { - organizationId: this.cipher.organizationId, - }); - } - - showGetPremium() { - if (this.canAccessPremium) { - return; - } - if (this.cipher.organizationUseTotp) { - this.upgradeOrganization(); - } else { - this.premiumRequired(); - } - } - - viewHistory() { - this.viewingPasswordHistory = !this.viewingPasswordHistory; - } - - protected cleanUp() { - if (this.totpInterval) { - window.clearInterval(this.totpInterval); - } - } - - protected async totpUpdateCode() { - if ( - this.cipher == null || - this.cipher.type !== CipherType.Login || - this.cipher.login.totp == null - ) { - if (this.totpInterval) { - window.clearInterval(this.totpInterval); - } - return; - } - - const totpResponse = await firstValueFrom(this.totpService.getCode$(this.cipher.login.totp)); - this.totpCode = totpResponse?.code; - if (this.totpCode != null) { - if (this.totpCode.length > 4) { - const half = Math.floor(this.totpCode.length / 2); - this.totpCodeFormatted = - this.totpCode.substring(0, half) + " " + this.totpCode.substring(half); - } else { - this.totpCodeFormatted = this.totpCode; - } - } else { - this.totpCodeFormatted = null; - if (this.totpInterval) { - window.clearInterval(this.totpInterval); - } - } - } - - protected allowOwnershipAssignment() { - return ( - (!this.editMode || this.cloneMode) && - this.ownershipOptions != null && - (this.ownershipOptions.length > 1 || !this.allowPersonal) - ); - } - - protected showTotp() { - return ( - this.cipher.type === CipherType.Login && - this.cipher.login.totp && - this.organization?.productTierType != ProductTierType.Free && - (this.cipher.organizationUseTotp || this.canAccessPremium) - ); - } - - private async totpTick(intervalSeconds: number) { - const epoch = Math.round(new Date().getTime() / 1000.0); - const mod = epoch % intervalSeconds; - - this.totpSec = intervalSeconds - mod; - this.totpDash = +(Math.round(((78.6 / intervalSeconds) * mod + "e+2") as any) + "e-2"); - this.totpLow = this.totpSec <= 7; - if (mod === 0) { - await this.totpUpdateCode(); - } - } -} diff --git a/apps/web/src/app/vault/individual-vault/attachments.component.html b/apps/web/src/app/vault/individual-vault/attachments.component.html deleted file mode 100644 index d657cd7f6c3..00000000000 --- a/apps/web/src/app/vault/individual-vault/attachments.component.html +++ /dev/null @@ -1,120 +0,0 @@ - diff --git a/apps/web/src/app/vault/individual-vault/attachments.component.ts b/apps/web/src/app/vault/individual-vault/attachments.component.ts deleted file mode 100644 index c6079dbe78f..00000000000 --- a/apps/web/src/app/vault/individual-vault/attachments.component.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Component } from "@angular/core"; - -import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/angular/vault/components/attachments.component"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; -import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view"; -import { DialogService, ToastService } from "@bitwarden/components"; -import { KeyService } from "@bitwarden/key-management"; - -@Component({ - selector: "app-vault-attachments", - templateUrl: "attachments.component.html", -}) -export class AttachmentsComponent extends BaseAttachmentsComponent { - protected override componentName = "app-vault-attachments"; - - constructor( - cipherService: CipherService, - i18nService: I18nService, - keyService: KeyService, - encryptService: EncryptService, - stateService: StateService, - platformUtilsService: PlatformUtilsService, - apiService: ApiService, - logService: LogService, - fileDownloadService: FileDownloadService, - dialogService: DialogService, - billingAccountProfileStateService: BillingAccountProfileStateService, - accountService: AccountService, - toastService: ToastService, - ) { - super( - cipherService, - i18nService, - keyService, - encryptService, - platformUtilsService, - apiService, - window, - logService, - stateService, - fileDownloadService, - dialogService, - billingAccountProfileStateService, - accountService, - toastService, - ); - } - - protected async reupload(attachment: AttachmentView) { - if (this.showFixOldAttachments(attachment)) { - await this.reuploadCipherAttachment(attachment, false); - } - } - - protected showFixOldAttachments(attachment: AttachmentView) { - return attachment.key == null && this.cipher.organizationId == null; - } -} diff --git a/apps/web/src/app/vault/org-vault/add-edit.component.ts b/apps/web/src/app/vault/org-vault/add-edit.component.ts deleted file mode 100644 index 89f3b79f1fb..00000000000 --- a/apps/web/src/app/vault/org-vault/add-edit.component.ts +++ /dev/null @@ -1,138 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { DatePipe } from "@angular/common"; -import { Component } from "@angular/core"; -import { firstValueFrom } from "rxjs"; - -import { CollectionService } from "@bitwarden/admin-console/common"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { AuditService } from "@bitwarden/common/abstractions/audit.service"; -import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; -import { UserId } from "@bitwarden/common/types/guid"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; -import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; -import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; -import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; -import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; -import { DialogService, ToastService } from "@bitwarden/components"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; -import { PasswordRepromptService, SshImportPromptService } from "@bitwarden/vault"; - -import { AddEditComponent as BaseAddEditComponent } from "../individual-vault/add-edit.component"; - -@Component({ - selector: "app-org-vault-add-edit", - templateUrl: "../individual-vault/add-edit.component.html", -}) -export class AddEditComponent extends BaseAddEditComponent { - originalCipher: Cipher = null; - protected override componentName = "app-org-vault-add-edit"; - - constructor( - cipherService: CipherService, - folderService: FolderService, - i18nService: I18nService, - platformUtilsService: PlatformUtilsService, - auditService: AuditService, - accountService: AccountService, - collectionService: CollectionService, - totpService: TotpService, - passwordGenerationService: PasswordGenerationServiceAbstraction, - private apiService: ApiService, - messagingService: MessagingService, - eventCollectionService: EventCollectionService, - policyService: PolicyService, - logService: LogService, - passwordRepromptService: PasswordRepromptService, - organizationService: OrganizationService, - dialogService: DialogService, - datePipe: DatePipe, - configService: ConfigService, - billingAccountProfileStateService: BillingAccountProfileStateService, - cipherAuthorizationService: CipherAuthorizationService, - toastService: ToastService, - sdkService: SdkService, - sshImportPromptService: SshImportPromptService, - ) { - super( - cipherService, - folderService, - i18nService, - platformUtilsService, - auditService, - accountService, - collectionService, - totpService, - passwordGenerationService, - messagingService, - eventCollectionService, - policyService, - organizationService, - logService, - passwordRepromptService, - dialogService, - datePipe, - configService, - billingAccountProfileStateService, - cipherAuthorizationService, - toastService, - sdkService, - sshImportPromptService, - ); - } - - protected loadCollections() { - if (!this.organization.canEditAllCiphers) { - return super.loadCollections(); - } - return Promise.resolve(this.collections); - } - - protected async loadCipher() { - this.isAdminConsoleAction = true; - const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - // Calling loadCipher first to assess if the cipher is unassigned. If null use apiService getCipherAdmin - const firstCipherCheck = await super.loadCipher(activeUserId); - - if (!this.organization.canEditAllCiphers && firstCipherCheck != null) { - return firstCipherCheck; - } - const response = await this.apiService.getCipherAdmin(this.cipherId); - const data = new CipherData(response); - - data.edit = true; - const cipher = new Cipher(data); - this.originalCipher = cipher; - return cipher; - } - - protected encryptCipher(userId: UserId) { - if (!this.organization.canEditAllCiphers) { - return super.encryptCipher(userId); - } - - return this.cipherService.encrypt(this.cipher, userId, null, null, this.originalCipher); - } - - protected async deleteCipher() { - if (!this.organization.canEditAllCiphers) { - const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - return super.deleteCipher(activeUserId); - } - return this.cipher.isDeleted - ? this.apiService.deleteCipherAdmin(this.cipherId) - : this.apiService.putDeleteCipherAdmin(this.cipherId); - } -} diff --git a/apps/web/src/app/vault/org-vault/attachments.component.ts b/apps/web/src/app/vault/org-vault/attachments.component.ts deleted file mode 100644 index c2ad82bc27a..00000000000 --- a/apps/web/src/app/vault/org-vault/attachments.component.ts +++ /dev/null @@ -1,105 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, OnInit } from "@angular/core"; -import { firstValueFrom } from "rxjs"; - -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; -import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { UserId } from "@bitwarden/common/types/guid"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; -import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; -import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view"; -import { DialogService, ToastService } from "@bitwarden/components"; -import { KeyService } from "@bitwarden/key-management"; - -import { AttachmentsComponent as BaseAttachmentsComponent } from "../individual-vault/attachments.component"; - -@Component({ - selector: "app-org-vault-attachments", - templateUrl: "../individual-vault/attachments.component.html", -}) -export class AttachmentsComponent extends BaseAttachmentsComponent implements OnInit { - viewOnly = false; - organization: Organization; - - constructor( - cipherService: CipherService, - i18nService: I18nService, - keyService: KeyService, - encryptService: EncryptService, - stateService: StateService, - platformUtilsService: PlatformUtilsService, - apiService: ApiService, - logService: LogService, - fileDownloadService: FileDownloadService, - dialogService: DialogService, - billingAccountProfileStateService: BillingAccountProfileStateService, - accountService: AccountService, - toastService: ToastService, - ) { - super( - cipherService, - i18nService, - keyService, - encryptService, - stateService, - platformUtilsService, - apiService, - logService, - fileDownloadService, - dialogService, - billingAccountProfileStateService, - accountService, - toastService, - ); - } - - async ngOnInit() { - await super.ngOnInit(); - } - - protected async reupload(attachment: AttachmentView) { - if (this.organization.canEditAllCiphers && this.showFixOldAttachments(attachment)) { - await super.reuploadCipherAttachment(attachment, true); - } - } - - protected async loadCipher() { - if (!this.organization.canEditAllCiphers) { - const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - return await super.loadCipher(activeUserId); - } - const response = await this.apiService.getCipherAdmin(this.cipherId); - return new Cipher(new CipherData(response)); - } - - protected saveCipherAttachment(file: File, userId: UserId) { - return this.cipherService.saveAttachmentWithServer( - this.cipherDomain, - file, - userId, - this.organization.canEditAllCiphers, - ); - } - - protected deleteCipherAttachment(attachmentId: string, userId: UserId) { - if (!this.organization.canEditAllCiphers) { - return super.deleteCipherAttachment(attachmentId, userId); - } - return this.apiService.deleteCipherAttachmentAdmin(this.cipherId, attachmentId); - } - - protected showFixOldAttachments(attachment: AttachmentView) { - return attachment.key == null && this.organization.canEditAllCiphers; - } -} From d5b7af75e9b0083902654a7c5b06f41a53ad4b4f Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Fri, 11 Apr 2025 15:16:30 -0400 Subject: [PATCH 02/47] [PM-14909] Build components for security task completion notification (#14230) * squash split component work from pm-14909 * fix typing --- apps/browser/src/_locales/en/messages.json | 25 ++ .../components/buttons/action-button.ts | 9 +- .../content/components/icons/external-link.ts | 22 ++ .../content/components/icons/index.ts | 2 + .../content/components/icons/keyhole.ts | 222 ++++++++++++++++++ .../lit-stories/icons/icons.lit-stories.ts | 2 + .../notification/body.lit-stories.ts | 2 +- .../body.lit-stories.ts} | 26 +- .../confirmation/container.lit-stories.ts | 55 +++++ .../confirmation/footer.lit-stories.ts | 36 +++ .../confirmation/message.lit-stories.ts | 37 +++ .../notification/container.lit-stories.ts | 61 +++++ .../notification/footer.lit-stories.ts | 2 +- .../notification/header.lit-stories.ts | 2 +- .../notification/confirmation-message.ts | 53 ----- .../{confirmation.ts => confirmation/body.ts} | 48 ++-- .../container.ts} | 69 ++++-- .../notification/confirmation/footer.ts | 59 +++++ .../notification/confirmation/message.ts | 83 +++++++ .../components/notification/container.ts | 24 +- .../abstractions/notification-bar.ts | 6 + apps/browser/src/autofill/notification/bar.ts | 6 +- 22 files changed, 730 insertions(+), 121 deletions(-) create mode 100644 apps/browser/src/autofill/content/components/icons/external-link.ts create mode 100644 apps/browser/src/autofill/content/components/icons/keyhole.ts rename apps/browser/src/autofill/content/components/lit-stories/notification/{confirmation.lit-stories.ts => confirmation/body.lit-stories.ts} (53%) create mode 100644 apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/container.lit-stories.ts create mode 100644 apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/footer.lit-stories.ts create mode 100644 apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/message.lit-stories.ts create mode 100644 apps/browser/src/autofill/content/components/lit-stories/notification/container.lit-stories.ts delete mode 100644 apps/browser/src/autofill/content/components/notification/confirmation-message.ts rename apps/browser/src/autofill/content/components/notification/{confirmation.ts => confirmation/body.ts} (63%) rename apps/browser/src/autofill/content/components/notification/{confirmation-container.ts => confirmation/container.ts} (66%) create mode 100644 apps/browser/src/autofill/content/components/notification/confirmation/footer.ts create mode 100644 apps/browser/src/autofill/content/components/notification/confirmation/message.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 586e7e1f2cf..f3b85496b75 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/autofill/content/components/buttons/action-button.ts b/apps/browser/src/autofill/content/components/buttons/action-button.ts index f0642d4233a..881b44b4785 100644 --- a/apps/browser/src/autofill/content/components/buttons/action-button.ts +++ b/apps/browser/src/autofill/content/components/buttons/action-button.ts @@ -1,5 +1,5 @@ import { css } from "@emotion/css"; -import { html } from "lit"; +import { html, TemplateResult } from "lit"; import { Theme } from "@bitwarden/common/platform/enums"; @@ -11,7 +11,7 @@ export function ActionButton({ theme, handleClick, }: { - buttonText: string; + buttonText: string | TemplateResult; disabled?: boolean; theme: Theme; handleClick: (e: Event) => void; @@ -63,4 +63,9 @@ const actionButtonStyles = ({ disabled, theme }: { disabled: boolean; theme: The color: ${themes[theme].text.contrast}; } `} + + svg { + width: fit-content; + height: 16px; + } `; diff --git a/apps/browser/src/autofill/content/components/icons/external-link.ts b/apps/browser/src/autofill/content/components/icons/external-link.ts new file mode 100644 index 00000000000..10c6d831025 --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/external-link.ts @@ -0,0 +1,22 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { IconProps } from "../common-types"; +import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; + +export function ExternalLink({ color, disabled, theme }: IconProps) { + const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; + + return html` + + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/icons/index.ts b/apps/browser/src/autofill/content/components/icons/index.ts index c4769a0e69d..4b6cb7abdd8 100644 --- a/apps/browser/src/autofill/content/components/icons/index.ts +++ b/apps/browser/src/autofill/content/components/icons/index.ts @@ -4,9 +4,11 @@ export { BrandIconContainer } from "./brand-icon-container"; export { Business } from "./business"; export { Close } from "./close"; export { ExclamationTriangle } from "./exclamation-triangle"; +export { ExternalLink } from "./external-link"; export { Family } from "./family"; export { Folder } from "./folder"; export { Globe } from "./globe"; +export { Keyhole } from "./keyhole"; export { PartyHorn } from "./party-horn"; export { PencilSquare } from "./pencil-square"; export { Shield } from "./shield"; diff --git a/apps/browser/src/autofill/content/components/icons/keyhole.ts b/apps/browser/src/autofill/content/components/icons/keyhole.ts new file mode 100644 index 00000000000..0294c0c8499 --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/keyhole.ts @@ -0,0 +1,222 @@ +import { html } from "lit"; + +import { ThemeTypes } from "@bitwarden/common/platform/enums"; + +import { IconProps } from "../common-types"; + +// This icon has static multi-colors for each theme +export function Keyhole({ theme }: IconProps) { + if (theme === ThemeTypes.Dark) { + return html` + + + + + + + + + + + + + + + + + + + + + + `; + } + + return html` + + + + + + + + + + + + + + + + + + + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts index 20c88a59246..8bd87ef6674 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts +++ b/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts @@ -57,9 +57,11 @@ export const BusinessIcon = createIconStory("Business"); export const BrandIcon = createIconStory("BrandIconContainer"); export const CloseIcon = createIconStory("Close"); export const ExclamationTriangleIcon = createIconStory("ExclamationTriangle"); +export const ExternalLinkIcon = createIconStory("ExternalLink"); export const FamilyIcon = createIconStory("Family"); export const FolderIcon = createIconStory("Folder"); export const GlobeIcon = createIconStory("Globe"); +export const KeyholeIcon = createIconStory("Keyhole"); export const PartyHornIcon = createIconStory("PartyHorn"); export const PencilSquareIcon = createIconStory("PencilSquare"); export const ShieldIcon = createIconStory("Shield"); diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts index e43bc08b920..32b4170d1da 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts @@ -16,7 +16,7 @@ type Args = { }; export default { - title: "Components/Notifications/Notification Body", + title: "Components/Notifications/Body", argTypes: { ciphers: { control: "object" }, theme: { control: "select", options: [...Object.values(ThemeTypes)] }, diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/body.lit-stories.ts similarity index 53% rename from apps/browser/src/autofill/content/components/lit-stories/notification/confirmation.lit-stories.ts rename to apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/body.lit-stories.ts index b3dee95efd0..4d9be06fd7e 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation.lit-stories.ts +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/body.lit-stories.ts @@ -1,29 +1,26 @@ import { Meta, StoryObj } from "@storybook/web-components"; -import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; +import { ThemeTypes } from "@bitwarden/common/platform/enums"; -import { NotificationConfirmationBody } from "../../notification/confirmation"; - -type Args = { - buttonText: string; - confirmationMessage: string; - handleOpenVault: () => void; - theme: Theme; - error: string; -}; +import { + NotificationConfirmationBody, + NotificationConfirmationBodyProps, +} from "../../../notification/confirmation/body"; export default { - title: "Components/Notifications/Notification Confirmation Body", + title: "Components/Notifications/Confirmation/Body", argTypes: { error: { control: "text" }, buttonText: { control: "text" }, confirmationMessage: { control: "text" }, + messageDetails: { control: "text" }, theme: { control: "select", options: [...Object.values(ThemeTypes)] }, }, args: { error: "", buttonText: "View", confirmationMessage: "[item name] updated in Bitwarden.", + messageDetails: "You can view it in your vault.", theme: ThemeTypes.Light, }, parameters: { @@ -32,10 +29,11 @@ export default { url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=485-20160&m=dev", }, }, -} as Meta; +} as Meta; -const Template = (args: Args) => NotificationConfirmationBody({ ...args }); +const Template = (args: NotificationConfirmationBodyProps) => + NotificationConfirmationBody({ ...args }); -export const Default: StoryObj = { +export const Default: StoryObj = { render: Template, }; diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/container.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/container.lit-stories.ts new file mode 100644 index 00000000000..ec7194004d8 --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/container.lit-stories.ts @@ -0,0 +1,55 @@ +import { Meta, StoryObj } from "@storybook/web-components"; + +import { ThemeTypes } from "@bitwarden/common/platform/enums"; + +import { NotificationTypes } from "../../../../../notification/abstractions/notification-bar"; +import { + NotificationConfirmationContainer, + NotificationConfirmationContainerProps, +} from "../../../notification/confirmation/container"; + +export default { + title: "Components/Notifications/Confirmation", + argTypes: { + error: { control: "text" }, + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + type: { control: "select", options: [NotificationTypes.Change, NotificationTypes.Add] }, + }, + args: { + error: "", + task: { + orgName: "Acme, Inc.", + remainingTasksCount: 0, + }, + handleCloseNotification: () => alert("Close notification action triggered"), + handleOpenTasks: () => alert("Open tasks action triggered"), + i18n: { + loginSaveSuccess: "Login saved", + loginUpdateSuccess: "Login updated", + loginUpdateTaskSuccessAdditional: + "Thank you for making your organization more secure. You have 3 more passwords to update.", + loginUpdateTaskSuccess: + "Great job! You took the steps to make you and your organization more secure.", + nextSecurityTaskAction: "Change next password", + saveFailure: "Error saving", + saveFailureDetails: "Oh no! We couldn't save this. Try entering the details manually.", + view: "View", + }, + type: NotificationTypes.Change, + username: "Acme, Inc. Login", + theme: ThemeTypes.Light, + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=485-20160&m=dev", + }, + }, +} as Meta; + +const Template = (args: NotificationConfirmationContainerProps) => + NotificationConfirmationContainer({ ...args }); + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/footer.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/footer.lit-stories.ts new file mode 100644 index 00000000000..953fb90d067 --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/footer.lit-stories.ts @@ -0,0 +1,36 @@ +import { Meta, StoryObj } from "@storybook/web-components"; +import { html } from "lit"; + +import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; + +import { + NotificationConfirmationFooter, + NotificationConfirmationFooterProps, +} from "../../../notification/confirmation/footer"; + +export default { + title: "Components/Notifications/Confirmation/Footer", + argTypes: { + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + }, + args: { + handleButtonClick: () => alert("Action button triggered"), + i18n: { + nextSecurityTaskAction: "Change next password", + }, + theme: ThemeTypes.Light, + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=32-4949&m=dev", + }, + }, +} as Meta; + +const Template = (args: NotificationConfirmationFooterProps) => + html`
${NotificationConfirmationFooter({ ...args })}
`; + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/message.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/message.lit-stories.ts new file mode 100644 index 00000000000..f01503b331f --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/message.lit-stories.ts @@ -0,0 +1,37 @@ +import { Meta, StoryObj } from "@storybook/web-components"; + +import { ThemeTypes } from "@bitwarden/common/platform/enums"; + +import { + NotificationConfirmationMessage, + NotificationConfirmationMessageProps, +} from "../../../notification/confirmation/message"; + +export default { + title: "Components/Notifications/Confirmation/Message", + argTypes: { + buttonText: { control: "text" }, + message: { control: "text" }, + messageDetails: { control: "text" }, + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + }, + args: { + buttonText: "View", + message: "[item name] updated in Bitwarden.", + messageDetails: "It was added to your vault.", + theme: ThemeTypes.Light, + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=485-20160&m=dev", + }, + }, +} as Meta; + +const Template = (args: NotificationConfirmationMessageProps) => + NotificationConfirmationMessage({ ...args }); + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/container.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/container.lit-stories.ts new file mode 100644 index 00000000000..351c971ec0e --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/container.lit-stories.ts @@ -0,0 +1,61 @@ +import { Meta, StoryObj } from "@storybook/web-components"; + +import { ThemeTypes } from "@bitwarden/common/platform/enums"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; + +import { NotificationTypes } from "../../../../notification/abstractions/notification-bar"; +import { NotificationContainer, NotificationContainerProps } from "../../notification/container"; + +export default { + title: "Components/Notifications", + argTypes: { + error: { control: "text" }, + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + type: { control: "select", options: [...Object.values(NotificationTypes)] }, + }, + args: { + error: "", + ciphers: [ + { + id: "1", + name: "Example Cipher", + type: CipherType.Login, + favorite: false, + reprompt: CipherRepromptType.None, + icon: { + imageEnabled: true, + image: "", + fallbackImage: "https://example.com/fallback.png", + icon: "icon-class", + }, + login: { username: "user@example.com" }, + }, + ], + i18n: { + loginSaveSuccess: "Login saved", + loginUpdateSuccess: "Login updated", + saveAction: "Save", + saveAsNewLoginAction: "Save as new login", + saveFailure: "Error saving", + saveFailureDetails: "Oh no! We couldn't save this. Try entering the details manually.", + updateLoginPrompt: "Update existing login?", + view: "View", + }, + type: NotificationTypes.Change, + username: "mockUsername", + theme: ThemeTypes.Light, + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=485-20160&m=dev", + }, + }, +} as Meta; + +const Template = (args: NotificationContainerProps) => NotificationContainer({ ...args }); + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/footer.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/footer.lit-stories.ts index ea2bbdc2e15..29d9955ec64 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/notification/footer.lit-stories.ts +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/footer.lit-stories.ts @@ -7,7 +7,7 @@ import { NotificationFooter, NotificationFooterProps } from "../../notification/ import { mockFolderData, mockOrganizationData } from "../mock-data"; export default { - title: "Components/Notifications/Notification Footer", + title: "Components/Notifications/Footer", argTypes: { notificationType: { control: "select", diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/header.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/header.lit-stories.ts index 49cc1e6bd8d..0857c99130e 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/notification/header.lit-stories.ts +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/header.lit-stories.ts @@ -12,7 +12,7 @@ type Args = { }; export default { - title: "Components/Notifications/Notification Header", + title: "Components/Notifications/Header", argTypes: { message: { control: "text" }, standalone: { control: "boolean" }, diff --git a/apps/browser/src/autofill/content/components/notification/confirmation-message.ts b/apps/browser/src/autofill/content/components/notification/confirmation-message.ts deleted file mode 100644 index d6f7ba3024d..00000000000 --- a/apps/browser/src/autofill/content/components/notification/confirmation-message.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { css } from "@emotion/css"; -import { html } from "lit"; - -import { Theme } from "@bitwarden/common/platform/enums"; - -import { themes } from "../constants/styles"; - -export function NotificationConfirmationMessage({ - buttonText, - confirmationMessage, - handleClick, - theme, -}: { - buttonText: string; - confirmationMessage: string; - handleClick: (e: Event) => void; - theme: Theme; -}) { - return html` - ${confirmationMessage} - ${buttonText} - `; -} - -const baseTextStyles = css` - flex-grow: 1; - overflow-x: hidden; - text-align: left; - text-overflow: ellipsis; - line-height: 24px; - font-family: "DM Sans", sans-serif; - font-size: 16px; -`; - -const notificationConfirmationMessageStyles = (theme: Theme) => css` - ${baseTextStyles} - color: ${themes[theme].text.main}; - font-weight: 400; -`; - -const notificationConfirmationButtonTextStyles = (theme: Theme) => css` - ${baseTextStyles} - color: ${themes[theme].primary[600]}; - font-weight: 700; - cursor: pointer; -`; diff --git a/apps/browser/src/autofill/content/components/notification/confirmation.ts b/apps/browser/src/autofill/content/components/notification/confirmation/body.ts similarity index 63% rename from apps/browser/src/autofill/content/components/notification/confirmation.ts rename to apps/browser/src/autofill/content/components/notification/confirmation/body.ts index 8c213a7663f..55d257b36f4 100644 --- a/apps/browser/src/autofill/content/components/notification/confirmation.ts +++ b/apps/browser/src/autofill/content/components/notification/confirmation/body.ts @@ -1,12 +1,12 @@ import createEmotion from "@emotion/css/create-instance"; -import { html } from "lit"; +import { html, nothing } from "lit"; import { Theme } from "@bitwarden/common/platform/enums"; -import { themes } from "../constants/styles"; -import { PartyHorn, Warning } from "../icons"; +import { themes } from "../../constants/styles"; +import { PartyHorn, Keyhole, Warning } from "../../icons"; -import { NotificationConfirmationMessage } from "./confirmation-message"; +import { NotificationConfirmationMessage } from "./message"; export const componentClassPrefix = "notification-confirmation-body"; @@ -14,31 +14,41 @@ const { css } = createEmotion({ key: componentClassPrefix, }); -export function NotificationConfirmationBody({ - buttonText, - error, - confirmationMessage, - theme, - handleOpenVault, -}: { - error?: string; +export type NotificationConfirmationBodyProps = { buttonText: string; confirmationMessage: string; + error?: string; + messageDetails?: string; + tasksAreComplete?: boolean; theme: Theme; handleOpenVault: (e: Event) => void; -}) { - const IconComponent = !error ? PartyHorn : Warning; +}; + +export function NotificationConfirmationBody({ + buttonText, + confirmationMessage, + error, + messageDetails, + tasksAreComplete, + theme, + handleOpenVault, +}: NotificationConfirmationBodyProps) { + const IconComponent = tasksAreComplete ? Keyhole : !error ? PartyHorn : Warning; + + const showConfirmationMessage = confirmationMessage || buttonText || messageDetails; + return html`
${IconComponent({ theme })}
- ${confirmationMessage && buttonText + ${showConfirmationMessage ? NotificationConfirmationMessage({ - handleClick: handleOpenVault, - confirmationMessage, - theme, buttonText, + message: confirmationMessage, + messageDetails, + theme, + handleClick: handleOpenVault, }) - : null} + : nothing}
`; } diff --git a/apps/browser/src/autofill/content/components/notification/confirmation-container.ts b/apps/browser/src/autofill/content/components/notification/confirmation/container.ts similarity index 66% rename from apps/browser/src/autofill/content/components/notification/confirmation-container.ts rename to apps/browser/src/autofill/content/components/notification/confirmation/container.ts index 0666859ac44..a071338af9a 100644 --- a/apps/browser/src/autofill/content/components/notification/confirmation-container.ts +++ b/apps/browser/src/autofill/content/components/notification/confirmation/container.ts @@ -1,42 +1,67 @@ import { css } from "@emotion/css"; -import { html } from "lit"; +import { html, nothing } from "lit"; import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; import { NotificationBarIframeInitData, - NotificationTypes, + NotificationTaskInfo, NotificationType, -} from "../../../notification/abstractions/notification-bar"; -import { themes, spacing } from "../constants/styles"; - -import { NotificationConfirmationBody } from "./confirmation"; + NotificationTypes, +} from "../../../../notification/abstractions/notification-bar"; +import { themes, spacing } from "../../constants/styles"; import { NotificationHeader, componentClassPrefix as notificationHeaderClassPrefix, -} from "./header"; +} from "../header"; + +import { NotificationConfirmationBody } from "./body"; +import { NotificationConfirmationFooter } from "./footer"; + +export type NotificationConfirmationContainerProps = NotificationBarIframeInitData & { + handleCloseNotification: (e: Event) => void; + handleOpenVault: (e: Event) => void; + handleOpenTasks: (e: Event) => void; +} & { + error?: string; + i18n: { [key: string]: string }; + task?: NotificationTaskInfo; + type: NotificationType; + username: string; +}; export function NotificationConfirmationContainer({ error, handleCloseNotification, handleOpenVault, + handleOpenTasks, i18n, + task, theme = ThemeTypes.Light, type, username, -}: NotificationBarIframeInitData & { - handleCloseNotification: (e: Event) => void; - handleOpenVault: (e: Event) => void; -} & { - error?: string; - i18n: { [key: string]: string }; - type: NotificationType; - username: string; -}) { +}: NotificationConfirmationContainerProps) { const headerMessage = getHeaderMessage(i18n, type, error); const confirmationMessage = getConfirmationMessage(i18n, username, type, error); const buttonText = error ? i18n.newItem : i18n.view; + let messageDetails: string | undefined; + let remainingTasksCount: number | undefined; + let tasksAreComplete: boolean = false; + + if (task) { + remainingTasksCount = task.remainingTasksCount || 0; + tasksAreComplete = remainingTasksCount === 0; + + messageDetails = + remainingTasksCount > 0 + ? chrome.i18n.getMessage("loginUpdateTaskSuccessAdditional", [ + task.orgName, + `${remainingTasksCount}`, + ]) + : chrome.i18n.getMessage("loginUpdateTaskSuccess", [task.orgName]); + } + return html`
${NotificationHeader({ @@ -47,10 +72,18 @@ export function NotificationConfirmationContainer({ ${NotificationConfirmationBody({ buttonText, confirmationMessage, - error: error, - handleOpenVault, + tasksAreComplete, + messageDetails, theme, + handleOpenVault, })} + ${remainingTasksCount + ? NotificationConfirmationFooter({ + i18n, + theme, + handleButtonClick: handleOpenTasks, + }) + : nothing}
`; } diff --git a/apps/browser/src/autofill/content/components/notification/confirmation/footer.ts b/apps/browser/src/autofill/content/components/notification/confirmation/footer.ts new file mode 100644 index 00000000000..e245d22c8e8 --- /dev/null +++ b/apps/browser/src/autofill/content/components/notification/confirmation/footer.ts @@ -0,0 +1,59 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { ActionButton } from "../../buttons/action-button"; +import { spacing, themes } from "../../constants/styles"; +import { ExternalLink } from "../../icons"; + +export type NotificationConfirmationFooterProps = { + i18n: { [key: string]: string }; + theme: Theme; + handleButtonClick: (event: Event) => void; +}; + +export function NotificationConfirmationFooter({ + i18n, + theme, + handleButtonClick, +}: NotificationConfirmationFooterProps) { + const primaryButtonText = i18n.nextSecurityTaskAction; + + return html` +
+ ${ActionButton({ + handleClick: handleButtonClick, + buttonText: AdditionalTasksButtonContent({ buttonText: primaryButtonText, theme }), + theme, + })} +
+ `; +} + +const notificationConfirmationFooterStyles = ({ theme }: { theme: Theme }) => css` + background-color: ${themes[theme].background.alt}; + padding: 0 ${spacing[3]} ${spacing[3]} ${spacing[3]}; + max-width: min-content; + + :last-child { + border-radius: 0 0 ${spacing["4"]} ${spacing["4"]}; + padding-bottom: ${spacing[4]}; + } +`; + +function AdditionalTasksButtonContent({ buttonText, theme }: { buttonText: string; theme: Theme }) { + return html` +
+ ${buttonText} + ${ExternalLink({ theme, color: themes[theme].text.contrast })} +
+ `; +} + +const additionalTasksButtonContentStyles = ({ theme }: { theme: Theme }) => css` + gap: ${spacing[2]}; + display: flex; + align-items: center; + white-space: nowrap; +`; diff --git a/apps/browser/src/autofill/content/components/notification/confirmation/message.ts b/apps/browser/src/autofill/content/components/notification/confirmation/message.ts new file mode 100644 index 00000000000..c018371caff --- /dev/null +++ b/apps/browser/src/autofill/content/components/notification/confirmation/message.ts @@ -0,0 +1,83 @@ +import { css } from "@emotion/css"; +import { html, nothing } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { themes, typography } from "../../constants/styles"; + +export type NotificationConfirmationMessageProps = { + buttonText?: string; + message?: string; + messageDetails?: string; + handleClick: (e: Event) => void; + theme: Theme; +}; + +export function NotificationConfirmationMessage({ + buttonText, + message, + messageDetails, + handleClick, + theme, +}: NotificationConfirmationMessageProps) { + return html` +
+ ${message || buttonText + ? html` + + ${message || nothing} + ${buttonText + ? html` + + ${buttonText} + + ` + : nothing} + + ` + : nothing} + ${messageDetails + ? html`
${messageDetails}
` + : nothing} +
+ `; +} + +const baseTextStyles = css` + flex-grow: 1; + overflow-x: hidden; + text-align: left; + text-overflow: ellipsis; + line-height: 24px; + font-family: "DM Sans", sans-serif; + font-size: 16px; +`; + +const notificationConfirmationMessageStyles = (theme: Theme) => css` + ${baseTextStyles} + + color: ${themes[theme].text.main}; + font-weight: 400; +`; + +const notificationConfirmationButtonTextStyles = (theme: Theme) => css` + ${baseTextStyles} + + color: ${themes[theme].primary[600]}; + font-weight: 700; + cursor: pointer; +`; + +const AdditionalMessageStyles = ({ theme }: { theme: Theme }) => css` + ${typography.body2} + + font-size: 14px; + color: ${themes[theme].text.muted}; +`; diff --git a/apps/browser/src/autofill/content/components/notification/container.ts b/apps/browser/src/autofill/content/components/notification/container.ts index 8d80dc9fb50..c29f58e116b 100644 --- a/apps/browser/src/autofill/content/components/notification/container.ts +++ b/apps/browser/src/autofill/content/components/notification/container.ts @@ -19,6 +19,18 @@ import { componentClassPrefix as notificationHeaderClassPrefix, } from "./header"; +export type NotificationContainerProps = NotificationBarIframeInitData & { + handleCloseNotification: (e: Event) => void; + handleSaveAction: (e: Event) => void; + handleEditOrUpdateAction: (e: Event) => void; +} & { + ciphers?: NotificationCipherData[]; + folders?: FolderView[]; + i18n: { [key: string]: string }; + organizations?: OrgView[]; + type: NotificationType; // @TODO typing override for generic `NotificationBarIframeInitData.type` +}; + export function NotificationContainer({ handleCloseNotification, handleEditOrUpdateAction, @@ -29,17 +41,7 @@ export function NotificationContainer({ organizations, theme = ThemeTypes.Light, type, -}: NotificationBarIframeInitData & { - handleCloseNotification: (e: Event) => void; - handleSaveAction: (e: Event) => void; - handleEditOrUpdateAction: (e: Event) => void; -} & { - ciphers?: NotificationCipherData[]; - folders?: FolderView[]; - i18n: { [key: string]: string }; - organizations?: OrgView[]; - type: NotificationType; // @TODO typing override for generic `NotificationBarIframeInitData.type` -}) { +}: NotificationContainerProps) { const headerMessage = getHeaderMessage(i18n, type); const showBody = true; diff --git a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts index c138776ed0e..7e2fdab04d3 100644 --- a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts +++ b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts @@ -11,6 +11,11 @@ const NotificationTypes = { type NotificationType = (typeof NotificationTypes)[keyof typeof NotificationTypes]; +type NotificationTaskInfo = { + orgName: string; + remainingTasksCount: number; +}; + type NotificationBarIframeInitData = { ciphers?: NotificationCipherData[]; folders?: FolderView[]; @@ -38,6 +43,7 @@ type NotificationBarWindowMessageHandlers = { }; export { + NotificationTaskInfo, NotificationTypes, NotificationType, NotificationBarIframeInitData, diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts index 139b4551a24..f544e75527c 100644 --- a/apps/browser/src/autofill/notification/bar.ts +++ b/apps/browser/src/autofill/notification/bar.ts @@ -7,7 +7,7 @@ import type { FolderView } from "@bitwarden/common/vault/models/view/folder.view import { AdjustNotificationBarMessageData } from "../background/abstractions/notification.background"; import { NotificationCipherData } from "../content/components/cipher/types"; import { OrgView } from "../content/components/common-types"; -import { NotificationConfirmationContainer } from "../content/components/notification/confirmation-container"; +import { NotificationConfirmationContainer } from "../content/components/notification/confirmation/container"; import { NotificationContainer } from "../content/components/notification/container"; import { buildSvgDomElement } from "../utils"; import { circleCheckIcon } from "../utils/svg-icons"; @@ -58,6 +58,9 @@ function getI18n() { loginSaveSuccessDetails: chrome.i18n.getMessage("loginSaveSuccessDetails"), loginUpdateSuccess: chrome.i18n.getMessage("loginUpdateSuccess"), loginUpdateSuccessDetails: chrome.i18n.getMessage("loginUpdatedSuccessDetails"), + loginUpdateTaskSuccess: chrome.i18n.getMessage("loginUpdateTaskSuccess"), + loginUpdateTaskSuccessAdditional: chrome.i18n.getMessage("loginUpdateTaskSuccessAdditional"), + nextSecurityTaskAction: chrome.i18n.getMessage("nextSecurityTaskAction"), newItem: chrome.i18n.getMessage("newItem"), never: chrome.i18n.getMessage("never"), notificationAddDesc: chrome.i18n.getMessage("notificationAddDesc"), @@ -369,6 +372,7 @@ function handleSaveCipherConfirmation(message: NotificationBarWindowMessage) { error, username: username ?? i18n.typeLogin, handleOpenVault: (e) => cipherId && openViewVaultItemPopout(e, cipherId), + handleOpenTasks: () => {}, }), document.body, ); From 2fd83f830db77960367b2ddbf427bb9fed97e74f Mon Sep 17 00:00:00 2001 From: Jakub Gilis Date: Fri, 11 Apr 2025 21:30:06 +0200 Subject: [PATCH 03/47] Properly handle message aborts during cleanup (#13841) Replace the FallbackRequestedError rejection pattern with direct AbortController.abort() calls when destroying the Messenger. This eliminates misleading console errors and ensures correct cancellation behavior. The FallbackRequestedError is intended specifically for user-requested WebAuthn fallbacks, not general message cleanup operations. Fixes GitHub issue #12663 --- .../autofill/fido2/content/messaging/messenger.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/apps/browser/src/autofill/fido2/content/messaging/messenger.ts b/apps/browser/src/autofill/fido2/content/messaging/messenger.ts index a5530d87a8e..ec7ff3bb7a4 100644 --- a/apps/browser/src/autofill/fido2/content/messaging/messenger.ts +++ b/apps/browser/src/autofill/fido2/content/messaging/messenger.ts @@ -1,7 +1,5 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { FallbackRequestedError } from "@bitwarden/common/platform/abstractions/fido2/fido2-client.service.abstraction"; - import { Message, MessageType } from "./message"; const SENDER = "bitwarden-webauthn"; @@ -126,17 +124,11 @@ export class Messenger { } }; - let onDestroyListener; - const destroyPromise: Promise = new Promise((_, reject) => { - onDestroyListener = () => reject(new FallbackRequestedError()); - this.onDestroy.addEventListener("destroy", onDestroyListener); - }); + const onDestroyListener = () => abortController.abort(); + this.onDestroy.addEventListener("destroy", onDestroyListener); try { - const handlerResponse = await Promise.race([ - this.handler(message, abortController), - destroyPromise, - ]); + const handlerResponse = await this.handler(message, abortController); port.postMessage({ ...handlerResponse, SENDER }); } catch (error) { port.postMessage({ From b90ede079d3671ea2c1a2a54cd133f01bf889c74 Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Fri, 11 Apr 2025 22:55:02 -0400 Subject: [PATCH 04/47] [PM-18888] Fix duo redirect URL checks (#14174) * fix(PM-18888) : Create more strict checking of redirectURL to protect against open redirect attacks using regex. * fix : modify comments and check for embedded credentials. * feat : add testability to duo-redirect connector * fix : fixing strict typing; Removed styling from duo-redirect.ts which allows us to test without adding additional files and configurations for jest. * fix : remove duo-redirect.scss --- apps/web/src/connectors/duo-redirect.scss | 1 - apps/web/src/connectors/duo-redirect.spec.ts | 51 +++++++++++++ apps/web/src/connectors/duo-redirect.ts | 76 ++++++++++++++------ apps/web/webpack.config.js | 2 +- 4 files changed, 105 insertions(+), 25 deletions(-) delete mode 100644 apps/web/src/connectors/duo-redirect.scss create mode 100644 apps/web/src/connectors/duo-redirect.spec.ts diff --git a/apps/web/src/connectors/duo-redirect.scss b/apps/web/src/connectors/duo-redirect.scss deleted file mode 100644 index a4c7f9b25b7..00000000000 --- a/apps/web/src/connectors/duo-redirect.scss +++ /dev/null @@ -1 +0,0 @@ -@import "../scss/styles.scss"; diff --git a/apps/web/src/connectors/duo-redirect.spec.ts b/apps/web/src/connectors/duo-redirect.spec.ts new file mode 100644 index 00000000000..c0498861ba0 --- /dev/null +++ b/apps/web/src/connectors/duo-redirect.spec.ts @@ -0,0 +1,51 @@ +import { redirectToDuoFrameless } from "./duo-redirect"; + +describe("duo-redirect", () => { + describe("redirectToDuoFrameless", () => { + beforeEach(() => { + Object.defineProperty(window, "location", { + value: { href: "" }, + writable: true, + }); + }); + + it("should redirect to a valid Duo URL", () => { + const validUrl = "https://api-123.duosecurity.com/auth"; + redirectToDuoFrameless(validUrl); + expect(window.location.href).toBe(validUrl); + }); + + it("should redirect to a valid Duo Federal URL", () => { + const validUrl = "https://api-123.duofederal.com/auth"; + redirectToDuoFrameless(validUrl); + expect(window.location.href).toBe(validUrl); + }); + + it("should throw an error for an invalid URL", () => { + const invalidUrl = "https://malicious-site.com"; + expect(() => redirectToDuoFrameless(invalidUrl)).toThrow("Invalid redirect URL"); + }); + + it("should throw an error for an malicious URL with valid redirect embedded", () => { + const invalidUrl = "https://malicious-site.com\\@api-123.duosecurity.com/auth"; + expect(() => redirectToDuoFrameless(invalidUrl)).toThrow("Invalid redirect URL"); + }); + + it("should throw an error for a non-HTTPS URL", () => { + const nonHttpsUrl = "http://api-123.duosecurity.com/auth"; + expect(() => redirectToDuoFrameless(nonHttpsUrl)).toThrow("Invalid redirect URL"); + }); + + it("should throw an error for a URL with an invalid hostname", () => { + const invalidHostnameUrl = "https://api-123.invalid.com"; + expect(() => redirectToDuoFrameless(invalidHostnameUrl)).toThrow("Invalid redirect URL"); + }); + + it("should throw an error for a URL with credentials", () => { + const UrlWithCredentials = "https://api-123.duosecurity.com:password@evil/attack"; + expect(() => redirectToDuoFrameless(UrlWithCredentials)).toThrow( + "Invalid redirect URL: embedded credentials not allowed", + ); + }); + }); +}); diff --git a/apps/web/src/connectors/duo-redirect.ts b/apps/web/src/connectors/duo-redirect.ts index c19e056d306..d1841247962 100644 --- a/apps/web/src/connectors/duo-redirect.ts +++ b/apps/web/src/connectors/duo-redirect.ts @@ -1,14 +1,8 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { getQsParam } from "./common"; import { TranslationService } from "./translation.service"; -// FIXME: Remove when updating file. Eslint update -// eslint-disable-next-line @typescript-eslint/no-require-imports -require("./duo-redirect.scss"); - const mobileDesktopCallback = "bitwarden://duo-callback"; -let localeService: TranslationService = null; +let localeService: TranslationService | null = null; window.addEventListener("load", async () => { const redirectUrl = getQsParam("duoFramelessUrl"); @@ -18,9 +12,18 @@ window.addEventListener("load", async () => { return; } - const client = getQsParam("client"); - const code = getQsParam("code"); - const state = getQsParam("state"); + const client: string | null = getQsParam("client"); + const code: string | null = getQsParam("code"); + const state: string | null = getQsParam("state"); + if (!client) { + throw new Error("client is null"); + } + if (!code) { + throw new Error("code is null"); + } + if (!state) { + throw new Error("state is null"); + } localeService = new TranslationService(navigator.language, "locales"); await localeService.init(); @@ -53,16 +56,28 @@ window.addEventListener("load", async () => { * validate the Duo AuthUrl and redirect to it. * @param redirectUrl the duo auth url */ -function redirectToDuoFrameless(redirectUrl: string) { - const validateUrl = new URL(redirectUrl); - const validDuoUrl = - validateUrl.protocol === "https:" && - (validateUrl.hostname.endsWith(".duosecurity.com") || - validateUrl.hostname.endsWith(".duofederal.com")); - - if (!validDuoUrl) { +export function redirectToDuoFrameless(redirectUrl: string) { + // Regex to match a valid duo redirect URL + /** + * This regex checks for the following: + * The string must start with "https://api-" + * Followed by a subdomain that can contain letters, numbers + * Followed by either "duosecurity.com" or "duofederal.com" + * This ensures that the redirect does not contain any malicious content + * and is a valid Duo URL. + * */ + const duoRedirectUrlRegex = /^https:\/\/api-[a-zA-Z0-9]+\.(duosecurity|duofederal)\.com/; + // Check if the redirect URL matches the regex + if (!duoRedirectUrlRegex.test(redirectUrl)) { throw new Error("Invalid redirect URL"); } + // At this point we know the URL to be valid, but we need to check for embedded credentials + const validateUrl = new URL(redirectUrl); + // URLs should not contain + // Check that no embedded credentials are present + if (validateUrl.username || validateUrl.password) { + throw new Error("Invalid redirect URL: embedded credentials not allowed"); + } window.location.href = decodeURIComponent(redirectUrl); } @@ -72,17 +87,23 @@ function redirectToDuoFrameless(redirectUrl: string) { * so browser, desktop, and mobile are not able to take advantage of the countdown timer or close button. */ function displayHandoffMessage(client: string) { - const content = document.getElementById("content"); + const content: HTMLElement | null = document.getElementById("content"); + if (!content) { + throw new Error("content element not found"); + } content.className = "text-center"; content.innerHTML = ""; const h1 = document.createElement("h1"); - const p = document.createElement("p"); + const p: HTMLElement = document.createElement("p"); + if (!localeService) { + throw new Error("localeService is not initialized"); + } h1.textContent = localeService.t("youSuccessfullyLoggedIn"); p.textContent = client == "web" - ? (p.textContent = localeService.t("thisWindowWillCloseIn5Seconds")) + ? localeService.t("thisWindowWillCloseIn5Seconds") : localeService.t("youMayCloseThisWindow"); h1.className = "font-weight-semibold"; @@ -102,11 +123,20 @@ function displayHandoffMessage(client: string) { }); content.appendChild(button); - // Countdown timer (closes tab upon completion) - let num = Number(p.textContent.match(/\d+/)[0]); + if (p.textContent === null) { + throw new Error("count down container is null"); + } + const counterString: string | null = p.textContent.match(/\d+/)?.[0] || null; + if (!counterString) { + throw new Error("count down time cannot be null"); + } + let num: number = Number(counterString); const interval = setInterval(() => { if (num > 1) { + if (p.textContent === null) { + throw new Error("count down container is null"); + } p.textContent = p.textContent.replace(String(num), String(num - 1)); num--; } else { diff --git a/apps/web/webpack.config.js b/apps/web/webpack.config.js index d172ea95c71..9ccccee21bf 100644 --- a/apps/web/webpack.config.js +++ b/apps/web/webpack.config.js @@ -142,7 +142,7 @@ const plugins = [ new HtmlWebpackPlugin({ template: "./src/connectors/duo-redirect.html", filename: "duo-redirect-connector.html", - chunks: ["connectors/duo-redirect"], + chunks: ["connectors/duo-redirect", "styles"], }), new HtmlWebpackPlugin({ template: "./src/404.html", From 8b64087b32d9e44105e2d037e600514de46627ef Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Mon, 14 Apr 2025 14:41:08 +0200 Subject: [PATCH 05/47] [PM-18040] Inject ipc content script dynamically (#13674) * feat: add content script manager * feat: inject into all pages * feat: only inject if flag is enabled * fix: wrong constructor parameters --- .../browser/src/background/main.background.ts | 3 ++ .../ipc/ipc-content-script-manager.service.ts | 42 +++++++++++++++++++ libs/common/src/enums/feature-flag.enum.ts | 6 +++ 3 files changed, 51 insertions(+) create mode 100644 apps/browser/src/platform/ipc/ipc-content-script-manager.service.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 709d64f2094..a5001e0c5b7 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -261,6 +261,7 @@ import VaultTimeoutService from "../key-management/vault-timeout/vault-timeout.s import { BrowserApi } from "../platform/browser/browser-api"; import { flagEnabled } from "../platform/flags"; import { IpcBackgroundService } from "../platform/ipc/ipc-background.service"; +import { IpcContentScriptManagerService } from "../platform/ipc/ipc-content-script-manager.service"; import { UpdateBadge } from "../platform/listeners/update-badge"; /* eslint-disable no-restricted-imports */ import { ChromeMessageSender } from "../platform/messaging/chrome-message.sender"; @@ -405,6 +406,7 @@ export default class MainBackground { inlineMenuFieldQualificationService: InlineMenuFieldQualificationService; taskService: TaskService; + ipcContentScriptManagerService: IpcContentScriptManagerService; ipcService: IpcService; onUpdatedRan: boolean; @@ -1314,6 +1316,7 @@ export default class MainBackground { this.inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); + this.ipcContentScriptManagerService = new IpcContentScriptManagerService(this.configService); this.ipcService = new IpcBackgroundService(this.logService); } diff --git a/apps/browser/src/platform/ipc/ipc-content-script-manager.service.ts b/apps/browser/src/platform/ipc/ipc-content-script-manager.service.ts new file mode 100644 index 00000000000..e5fe95e2018 --- /dev/null +++ b/apps/browser/src/platform/ipc/ipc-content-script-manager.service.ts @@ -0,0 +1,42 @@ +import { mergeMap } from "rxjs"; + +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; + +import { BrowserApi } from "../browser/browser-api"; + +const IPC_CONTENT_SCRIPT_ID = "ipc-content-script"; + +export class IpcContentScriptManagerService { + constructor(configService: ConfigService) { + if (!BrowserApi.isManifestVersion(3)) { + // IPC not supported on MV2 + return; + } + + configService + .getFeatureFlag$(FeatureFlag.IpcChannelFramework) + .pipe( + mergeMap(async (enabled) => { + if (!enabled) { + return; + } + + try { + await BrowserApi.unregisterContentScriptsMv3({ ids: [IPC_CONTENT_SCRIPT_ID] }); + } catch { + // Ignore errors + } + + await BrowserApi.registerContentScriptsMv3([ + { + id: IPC_CONTENT_SCRIPT_ID, + matches: ["https://*/*"], + js: ["content/ipc-content-script.js"], + }, + ]); + }), + ) + .subscribe(); + } +} diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index cd88a415caf..09708859ac8 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -55,6 +55,9 @@ export enum FeatureFlag { VaultBulkManagementAction = "vault-bulk-management-action", SecurityTasks = "security-tasks", CipherKeyEncryption = "cipher-key-encryption", + + /* Platform */ + IpcChannelFramework = "ipc-channel-framework", } export type AllowedFeatureFlagTypes = boolean | number | string; @@ -118,6 +121,9 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.PrivateKeyRegeneration]: FALSE, [FeatureFlag.UserKeyRotationV2]: FALSE, [FeatureFlag.PM4154_BulkEncryptionService]: FALSE, + + /* Platform */ + [FeatureFlag.IpcChannelFramework]: FALSE, } satisfies Record; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue; From 5cc3ed7c5fba06c04b67e1870ea71322f21d72f5 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 14 Apr 2025 14:42:08 +0200 Subject: [PATCH 06/47] Move nodecryptofunctionservice codeownership (#14209) --- .github/CODEOWNERS | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6c7c43c4dac..2f402b15dd5 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -83,8 +83,6 @@ libs/common/src/platform @bitwarden/team-platform-dev libs/common/spec @bitwarden/team-platform-dev libs/common/src/state-migrations @bitwarden/team-platform-dev libs/platform @bitwarden/team-platform-dev -# Node-specifc platform files -libs/node @bitwarden/team-key-management-dev # Web utils used across app and connectors apps/web/src/utils/ @bitwarden/team-platform-dev # Web core and shared files @@ -146,6 +144,8 @@ apps/cli/src/key-management @bitwarden/team-key-management-dev libs/key-management @bitwarden/team-key-management-dev libs/key-management-ui @bitwarden/team-key-management-dev libs/common/src/key-management @bitwarden/team-key-management-dev +# Node-cryptofunction service +libs/node @bitwarden/team-key-management-dev apps/desktop/desktop_native/core/src/biometric/ @bitwarden/team-key-management-dev apps/desktop/src/services/native-messaging.service.ts @bitwarden/team-key-management-dev From 8885f5da24c7ecebf98c947625bc8545dc840135 Mon Sep 17 00:00:00 2001 From: Alexander Aronov Date: Mon, 14 Apr 2025 14:42:41 +0200 Subject: [PATCH 07/47] [PM-19914][PM-19913] trim domains and long fields in forwarders (#14141) * PM-19913: Added max length to the generated_for and description peroperties in the FirefoxRelay API payload * [PM-19913] Added maxLength restriction to the website and generatedBy methods. Added maxLength limit of 200 to the description of addy.io --- .../integration/integration-context.spec.ts | 37 +++++++++++++++++++ .../tools/integration/integration-context.ts | 26 ++++++++++--- .../core/src/integration/addy-io.spec.ts | 4 ++ .../generator/core/src/integration/addy-io.ts | 2 +- .../src/integration/firefox-relay.spec.ts | 5 +++ .../core/src/integration/firefox-relay.ts | 4 +- 6 files changed, 70 insertions(+), 8 deletions(-) diff --git a/libs/common/src/tools/integration/integration-context.spec.ts b/libs/common/src/tools/integration/integration-context.spec.ts index 67a40afb337..33694aefea1 100644 --- a/libs/common/src/tools/integration/integration-context.spec.ts +++ b/libs/common/src/tools/integration/integration-context.spec.ts @@ -189,6 +189,33 @@ describe("IntegrationContext", () => { expect(result).toBe(""); }); + + it("extracts the hostname when extractHostname is true", () => { + const context = new IntegrationContext(EXAMPLE_META, null, i18n); + + const result = context.website( + { website: "https://www.example.com/path" }, + { extractHostname: true }, + ); + + expect(result).toBe("www.example.com"); + }); + + it("falls back to the full URL when Utils.getHost cannot extract the hostname", () => { + const context = new IntegrationContext(EXAMPLE_META, null, i18n); + + const result = context.website({ website: "invalid-url" }, { extractHostname: true }); + + expect(result).toBe("invalid-url"); + }); + + it("truncates the website to maxLength", () => { + const context = new IntegrationContext(EXAMPLE_META, null, i18n); + + const result = context.website({ website: "www.example.com" }, { maxLength: 3 }); + + expect(result).toBe("www"); + }); }); describe("generatedBy", () => { @@ -211,5 +238,15 @@ describe("IntegrationContext", () => { expect(result).toBe("result"); expect(i18n.t).toHaveBeenCalledWith("forwarderGeneratedByWithWebsite", "www.example.com"); }); + + it("truncates generated text to maxLength", () => { + const context = new IntegrationContext(EXAMPLE_META, null, i18n); + i18n.t.mockReturnValue("This is the result text"); + + const result = context.generatedBy({ website: null }, { maxLength: 4 }); + + expect(result).toBe("This"); + expect(i18n.t).toHaveBeenCalledWith("forwarderGeneratedBy", ""); + }); }); }); diff --git a/libs/common/src/tools/integration/integration-context.ts b/libs/common/src/tools/integration/integration-context.ts index 40648df6803..49edafc026b 100644 --- a/libs/common/src/tools/integration/integration-context.ts +++ b/libs/common/src/tools/integration/integration-context.ts @@ -79,24 +79,40 @@ export class IntegrationContext { /** look up the website the integration is working with. * @param request supplies information about the state of the extension site + * @param options optional parameters + * @param options.extractHostname when `true`, tries to extract the hostname from the website URL, returns full URL otherwise + * @param options.maxLength limits the length of the return value * @returns The website or an empty string if a website isn't available * @remarks `website` is usually supplied when generating a credential from the vault */ - website(request: IntegrationRequest) { - return request.website ?? ""; + website( + request: IntegrationRequest, + options?: { extractHostname?: boolean; maxLength?: number }, + ) { + let url = request.website ?? ""; + if (options?.extractHostname) { + url = Utils.getHost(url) ?? url; + } + return url.slice(0, options?.maxLength); } /** look up localized text indicating Bitwarden requested the forwarding address. * @param request supplies information about the state of the extension site + * @param options optional parameters + * @param options.extractHostname when `true`, extracts the hostname from the website URL + * @param options.maxLength limits the length of the return value * @returns localized text describing a generated forwarding address */ - generatedBy(request: IntegrationRequest) { - const website = this.website(request); + generatedBy( + request: IntegrationRequest, + options?: { extractHostname?: boolean; maxLength?: number }, + ) { + const website = this.website(request, { extractHostname: options?.extractHostname ?? false }); const descriptionId = website === "" ? "forwarderGeneratedBy" : "forwarderGeneratedByWithWebsite"; const description = this.i18n.t(descriptionId, website); - return description; + return description.slice(0, options?.maxLength); } } diff --git a/libs/tools/generator/core/src/integration/addy-io.spec.ts b/libs/tools/generator/core/src/integration/addy-io.spec.ts index 9c816330616..40d17e9d888 100644 --- a/libs/tools/generator/core/src/integration/addy-io.spec.ts +++ b/libs/tools/generator/core/src/integration/addy-io.spec.ts @@ -55,6 +55,10 @@ describe("Addy.io forwarder", () => { const result = AddyIo.forwarder.createForwardingEmail.body(null, context); + expect(context.generatedBy).toHaveBeenCalledWith(null, { + extractHostname: true, + maxLength: 200, + }); expect(result).toEqual({ domain: "domain", description: "generated by", diff --git a/libs/tools/generator/core/src/integration/addy-io.ts b/libs/tools/generator/core/src/integration/addy-io.ts index 631c5fdb510..93ffed3392a 100644 --- a/libs/tools/generator/core/src/integration/addy-io.ts +++ b/libs/tools/generator/core/src/integration/addy-io.ts @@ -39,7 +39,7 @@ const createForwardingEmail = Object.freeze({ body(request: IntegrationRequest, context: ForwarderContext) { return { domain: context.emailDomain(), - description: context.generatedBy(request), + description: context.generatedBy(request, { extractHostname: true, maxLength: 200 }), }; }, hasJsonPayload(response: Response) { diff --git a/libs/tools/generator/core/src/integration/firefox-relay.spec.ts b/libs/tools/generator/core/src/integration/firefox-relay.spec.ts index ed487b7f49f..08798b154b3 100644 --- a/libs/tools/generator/core/src/integration/firefox-relay.spec.ts +++ b/libs/tools/generator/core/src/integration/firefox-relay.spec.ts @@ -56,6 +56,11 @@ describe("Firefox Relay forwarder", () => { const result = FirefoxRelay.forwarder.createForwardingEmail.body(null, context); + expect(context.website).toHaveBeenCalledWith(null, { maxLength: 255 }); + expect(context.generatedBy).toHaveBeenCalledWith(null, { + extractHostname: true, + maxLength: 64, + }); expect(result).toEqual({ enabled: true, generated_for: "website", diff --git a/libs/tools/generator/core/src/integration/firefox-relay.ts b/libs/tools/generator/core/src/integration/firefox-relay.ts index 9f40a3631ff..f80de0c95dd 100644 --- a/libs/tools/generator/core/src/integration/firefox-relay.ts +++ b/libs/tools/generator/core/src/integration/firefox-relay.ts @@ -33,8 +33,8 @@ const createForwardingEmail = Object.freeze({ body(request: IntegrationRequest, context: ForwarderContext) { return { enabled: true, - generated_for: context.website(request), - description: context.generatedBy(request), + generated_for: context.website(request, { maxLength: 255 }), + description: context.generatedBy(request, { extractHostname: true, maxLength: 64 }), }; }, hasJsonPayload(response: Response) { From f1a2acb0b9b69e9eb854f093362e7c462e31dbb0 Mon Sep 17 00:00:00 2001 From: Alec Rippberger <127791530+alec-livefront@users.noreply.github.com> Date: Mon, 14 Apr 2025 09:37:52 -0500 Subject: [PATCH 08/47] fix: [PM-20180] add OrganizationDuo to dialog title function Fix issue where modal was not displayed when clicking Manage option for 2FA on Organizations. This adds the OrganizationDuo case to the dialogTitle method to properly handle this provider type. PM-20180 --- .../app/auth/settings/two-factor/two-factor-verify.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-verify.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-verify.component.ts index 98cb7199d2a..a153a9ec56a 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-verify.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-verify.component.ts @@ -99,6 +99,7 @@ export class TwoFactorVerifyComponent { case -1 as TwoFactorProviderType: return this.i18nService.t("recoveryCodeTitle"); case TwoFactorProviderType.Duo: + case TwoFactorProviderType.OrganizationDuo: return "Duo"; case TwoFactorProviderType.Email: return this.i18nService.t("emailTitle"); From 7e621be6cb331ebce55c06a801dbdb06766da348 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Mon, 14 Apr 2025 10:46:58 -0500 Subject: [PATCH 09/47] [PM-18969] CSV importers should create nested collections (#14007) --- .../src/importers/base-importer.spec.ts | 164 ++++++++++++++++++ libs/importer/src/importers/base-importer.ts | 13 +- .../bitwarden/bitwarden-csv-importer.ts | 39 +---- .../keeper/keeper-csv-importer.spec.ts | 33 +++- ...etwrix-passwordsecure-csv-importer.spec.ts | 19 +- .../passwordxp-csv-importer.spec.ts | 23 ++- .../importers/roboform-csv-importer.spec.ts | 17 +- .../spec-data/keeper-csv/testdata.csv.ts | 6 + .../spec-data/netwrix-csv/login-export.csv.ts | 3 + .../passwordxp-with-folders.csv.ts | 14 ++ .../spec-data/roboform-csv/with-folders.ts | 7 + 11 files changed, 303 insertions(+), 35 deletions(-) diff --git a/libs/importer/src/importers/base-importer.spec.ts b/libs/importer/src/importers/base-importer.spec.ts index 309bb7ca8c4..4e3cdb355be 100644 --- a/libs/importer/src/importers/base-importer.spec.ts +++ b/libs/importer/src/importers/base-importer.spec.ts @@ -1,6 +1,9 @@ import { CipherType } from "@bitwarden/common/vault/enums"; import { CardView } from "@bitwarden/common/vault/models/view/card.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; + +import { ImportResult } from "../models"; import { BaseImporter } from "./base-importer"; @@ -16,8 +19,169 @@ class FakeBaseImporter extends BaseImporter { parseXml(data: string): Document { return super.parseXml(data); } + + processFolder(result: ImportResult, folderName: string, addRelationship: boolean = true): void { + return super.processFolder(result, folderName, addRelationship); + } } +describe("processFolder method", () => { + let result: ImportResult; + const importer = new FakeBaseImporter(); + + beforeEach(() => { + result = { + folders: [], + folderRelationships: [], + collections: [], + collectionRelationships: [], + ciphers: [], + success: false, + errorMessage: "", + }; + }); + + it("should add a new folder and relationship when folderName is unique", () => { + // arrange + // a folder exists - but it is not the same as the one we are importing + result = { + folders: [{ name: "ABC" } as FolderView], + folderRelationships: [], + collections: [], + collectionRelationships: [], + ciphers: [{ name: "cipher1", id: "cipher1" } as CipherView], + success: false, + errorMessage: "", + }; + importer.processFolder(result, "Folder1"); + + expect(result.folders).toHaveLength(2); + expect(result.folders[0].name).toBe("ABC"); + expect(result.folders[1].name).toBe("Folder1"); + expect(result.folderRelationships).toHaveLength(1); + expect(result.folderRelationships[0]).toEqual([1, 1]); // cipher1 -> Folder1 + }); + + it("should not add duplicate folders and should add relationships", () => { + // setup + // folder called "Folder1" already exists + result = { + folders: [{ name: "Folder1" } as FolderView], + folderRelationships: [], + collections: [], + collectionRelationships: [], + ciphers: [{ name: "cipher1", id: "cipher1" } as CipherView], + success: false, + errorMessage: "", + }; + + // import an existing folder should not add to the result.folders + importer.processFolder(result, "Folder1"); + + expect(result.folders).toHaveLength(1); + expect(result.folders[0].name).toBe("Folder1"); + expect(result.folderRelationships).toHaveLength(1); + expect(result.folderRelationships[0]).toEqual([1, 0]); // cipher1 -> folder1 + }); + + it("should create parent folders for nested folder names but not duplicates", () => { + // arrange + result = { + folders: [ + { name: "Ancestor/Parent/Child" } as FolderView, + { name: "Ancestor" } as FolderView, + ], + folderRelationships: [], + collections: [], + collectionRelationships: [], + ciphers: [{ name: "cipher1", id: "cipher1" } as CipherView], + success: false, + errorMessage: "", + }; + + // act + // importing an existing folder with a relationship should not change the result.folders + // nor should it change the result.folderRelationships + importer.processFolder(result, "Ancestor/Parent/Child/Grandchild/GreatGrandchild"); + + expect(result.folders).toHaveLength(5); + expect(result.folders.map((f) => f.name)).toEqual([ + "Ancestor/Parent/Child", + "Ancestor", + "Ancestor/Parent/Child/Grandchild/GreatGrandchild", + "Ancestor/Parent/Child/Grandchild", + "Ancestor/Parent", + ]); + expect(result.folderRelationships).toHaveLength(1); + expect(result.folderRelationships[0]).toEqual([1, 2]); // cipher1 -> grandchild + }); + + it("should not affect existing relationships", () => { + // arrange + // "Parent" is a folder with no relationship + // "Child" is a folder with 2 ciphers + result = { + folders: [{ name: "Parent" } as FolderView, { name: "Parent/Child" } as FolderView], + folderRelationships: [ + [1, 1], + [2, 1], + ], + collections: [], + collectionRelationships: [], + ciphers: [ + { name: "cipher1", id: "cipher1" } as CipherView, + { name: "cipher2", id: "cipher2" } as CipherView, + { name: "cipher3", id: "cipher3" } as CipherView, + ], + success: false, + errorMessage: "", + }; + + // act + // importing an existing folder with a relationship should not change the result.folders + // nor should it change the result.folderRelationships + importer.processFolder(result, "Parent/Child/Grandchild"); + + expect(result.folders).toHaveLength(3); + expect(result.folders.map((f) => f.name)).toEqual([ + "Parent", + "Parent/Child", + "Parent/Child/Grandchild", + ]); + expect(result.folderRelationships).toHaveLength(3); + expect(result.folderRelationships[0]).toEqual([1, 1]); // cipher1 -> child + expect(result.folderRelationships[1]).toEqual([2, 1]); // cipher2 -> child + expect(result.folderRelationships[2]).toEqual([3, 2]); // cipher3 -> grandchild + }); + + it("should not add relationships if addRelationship is false", () => { + importer.processFolder(result, "Folder1", false); + + expect(result.folders).toHaveLength(1); + expect(result.folders[0].name).toBe("Folder1"); + expect(result.folderRelationships).toHaveLength(0); + }); + + it("should replace backslashes with forward slashes in folder names", () => { + importer.processFolder(result, "Parent\\Child\\Grandchild"); + + expect(result.folders).toHaveLength(3); + expect(result.folders.map((f) => f.name)).toEqual([ + "Parent/Child/Grandchild", + "Parent/Child", + "Parent", + ]); + }); + + it("should handle empty or null folder names gracefully", () => { + importer.processFolder(result, null); + importer.processFolder(result, ""); + + expect(result.folders).toHaveLength(0); + expect(result.folderRelationships).toHaveLength(0); + }); +}); + describe("BaseImporter class", () => { const importer = new FakeBaseImporter(); let cipher: CipherView; diff --git a/libs/importer/src/importers/base-importer.ts b/libs/importer/src/importers/base-importer.ts index 90af5344cfc..0594b6014e8 100644 --- a/libs/importer/src/importers/base-importer.ts +++ b/libs/importer/src/importers/base-importer.ts @@ -366,7 +366,7 @@ export abstract class BaseImporter { let folderIndex = result.folders.length; // Replace backslashes with forward slashes, ensuring we create sub-folders - folderName = folderName.replace("\\", "/"); + folderName = folderName.replace(/\\/g, "/"); let addFolder = true; for (let i = 0; i < result.folders.length; i++) { @@ -387,6 +387,17 @@ export abstract class BaseImporter { if (addRelationship) { result.folderRelationships.push([result.ciphers.length, folderIndex]); } + + // if the folder name is a/b/c/d, we need to create a/b/c and a/b and a + const parts = folderName.split("/"); + for (let i = parts.length - 1; i > 0; i--) { + const parentName = parts.slice(0, i).join("/") as string; + if (result.folders.find((c) => c.name === parentName) == null) { + const folder = new FolderView(); + folder.name = parentName; + result.folders.push(folder); + } + } } protected convertToNoteIfNeeded(cipher: CipherView) { diff --git a/libs/importer/src/importers/bitwarden/bitwarden-csv-importer.ts b/libs/importer/src/importers/bitwarden/bitwarden-csv-importer.ts index fab47b30b1a..abda9a04a8a 100644 --- a/libs/importer/src/importers/bitwarden/bitwarden-csv-importer.ts +++ b/libs/importer/src/importers/bitwarden/bitwarden-csv-importer.ts @@ -1,6 +1,5 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { CollectionView } from "@bitwarden/admin-console/common"; import { FieldType, SecureNoteType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -25,35 +24,11 @@ export class BitwardenCsvImporter extends BaseImporter implements Importer { if (this.organization && !this.isNullOrWhitespace(value.collections)) { const collections = (value.collections as string).split(","); collections.forEach((col) => { - let addCollection = true; - let collectionIndex = result.collections.length; - - for (let i = 0; i < result.collections.length; i++) { - if (result.collections[i].name === col) { - addCollection = false; - collectionIndex = i; - break; - } - } - - if (addCollection) { - const collection = new CollectionView(); - collection.name = col; - result.collections.push(collection); - } - - result.collectionRelationships.push([result.ciphers.length, collectionIndex]); - - // if the collection name is a/b/c/d, we need to create a/b/c and a/b and a - const parts = col.split("/"); - for (let i = parts.length - 1; i > 0; i--) { - const parentCollectionName = parts.slice(0, i).join("/") as string; - if (result.collections.find((c) => c.name === parentCollectionName) == null) { - const parentCollection = new CollectionView(); - parentCollection.name = parentCollectionName; - result.collections.push(parentCollection); - } - } + // here processFolder is used to create collections + // In an Organization folders are converted to collections + // see line just before this function terminates + // where all folders are turned to collections + this.processFolder(result, col); }); } else if (!this.organization) { this.processFolder(result, value.folder); @@ -125,6 +100,10 @@ export class BitwardenCsvImporter extends BaseImporter implements Importer { result.ciphers.push(cipher); }); + if (this.organization) { + this.moveFoldersToCollections(result); + } + result.success = true; return Promise.resolve(result); } diff --git a/libs/importer/src/importers/keeper/keeper-csv-importer.spec.ts b/libs/importer/src/importers/keeper/keeper-csv-importer.spec.ts index 69655eb9177..d7a4d487bcb 100644 --- a/libs/importer/src/importers/keeper/keeper-csv-importer.spec.ts +++ b/libs/importer/src/importers/keeper/keeper-csv-importer.spec.ts @@ -1,6 +1,9 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { testData as TestData } from "../spec-data/keeper-csv/testdata.csv"; +import { + testData as TestData, + testDataMultiCollection, +} from "../spec-data/keeper-csv/testdata.csv"; import { KeeperCsvImporter } from "./keeper-csv-importer"; @@ -121,4 +124,32 @@ describe("Keeper CSV Importer", () => { expect(result.collectionRelationships[1]).toEqual([1, 0]); expect(result.collectionRelationships[2]).toEqual([2, 1]); }); + + it("should create collections tree, with child collections and relationships", async () => { + importer.organizationId = Utils.newGuid(); + const result = await importer.parse(testDataMultiCollection); + expect(result != null).toBe(true); + + const collections = result.collections; + expect(collections).not.toBeNull(); + expect(collections.length).toBe(3); + + // collection with the cipher + const collections1 = collections.shift(); + expect(collections1.name).toBe("Foo/Baz/Bar"); + + //second level collection + const collections2 = collections.shift(); + expect(collections2.name).toBe("Foo/Baz"); + + //third level + const collections3 = collections.shift(); + expect(collections3.name).toBe("Foo"); + + // [Cipher, Folder] + expect(result.collectionRelationships.length).toBe(3); + expect(result.collectionRelationships[0]).toEqual([0, 0]); + expect(result.collectionRelationships[1]).toEqual([1, 1]); + expect(result.collectionRelationships[2]).toEqual([2, 2]); + }); }); diff --git a/libs/importer/src/importers/netwrix/netwrix-passwordsecure-csv-importer.spec.ts b/libs/importer/src/importers/netwrix/netwrix-passwordsecure-csv-importer.spec.ts index ff327daf04d..8736b3df0c8 100644 --- a/libs/importer/src/importers/netwrix/netwrix-passwordsecure-csv-importer.spec.ts +++ b/libs/importer/src/importers/netwrix/netwrix-passwordsecure-csv-importer.spec.ts @@ -1,6 +1,9 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { credentialsData } from "../spec-data/netwrix-csv/login-export.csv"; +import { + credentialsData, + credentialsDataWithFolders, +} from "../spec-data/netwrix-csv/login-export.csv"; import { NetwrixPasswordSecureCsvImporter } from "./netwrix-passwordsecure-csv-importer"; @@ -88,4 +91,18 @@ describe("Netwrix Password Secure CSV Importer", () => { expect(result.collectionRelationships[1]).toEqual([1, 1]); expect(result.collectionRelationships[2]).toEqual([2, 0]); }); + + it("should parse multiple collections", async () => { + importer.organizationId = Utils.newGuid(); + const result = await importer.parse(credentialsDataWithFolders); + + expect(result).not.toBeNull(); + expect(result.success).toBe(true); + expect(result.collections.length).toBe(3); + expect(result.collections[0].name).toBe("folder1/folder2/folder3"); + expect(result.collections[1].name).toBe("folder1/folder2"); + expect(result.collections[2].name).toBe("folder1"); + expect(result.collectionRelationships.length).toBe(1); + expect(result.collectionRelationships[0]).toEqual([0, 0]); + }); }); diff --git a/libs/importer/src/importers/passsordxp/passwordxp-csv-importer.spec.ts b/libs/importer/src/importers/passsordxp/passwordxp-csv-importer.spec.ts index 0decd1e2830..12cfbbe62bb 100644 --- a/libs/importer/src/importers/passsordxp/passwordxp-csv-importer.spec.ts +++ b/libs/importer/src/importers/passsordxp/passwordxp-csv-importer.spec.ts @@ -4,7 +4,10 @@ import { ImportResult } from "../../models/import-result"; import { dutchHeaders } from "../spec-data/passwordxp-csv/dutch-headers"; import { germanHeaders } from "../spec-data/passwordxp-csv/german-headers"; import { noFolder } from "../spec-data/passwordxp-csv/no-folder.csv"; -import { withFolders } from "../spec-data/passwordxp-csv/passwordxp-with-folders.csv"; +import { + withFolders, + withMultipleFolders, +} from "../spec-data/passwordxp-csv/passwordxp-with-folders.csv"; import { withoutFolders } from "../spec-data/passwordxp-csv/passwordxp-without-folders.csv"; import { PasswordXPCsvImporter } from "./passwordxp-csv-importer"; @@ -167,4 +170,22 @@ describe("PasswordXPCsvImporter", () => { expect(collectionRelationship).toEqual([4, 2]); collectionRelationship = result.collectionRelationships.shift(); }); + + it("should convert multi-level folders to collections when importing into an organization", async () => { + importer.organizationId = "someOrg"; + const result: ImportResult = await importer.parse(withMultipleFolders); + expect(result.success).toBe(true); + expect(result.ciphers.length).toBe(5); + + expect(result.collections.length).toBe(3); + expect(result.collections[0].name).toEqual("Test Folder"); + expect(result.collections[1].name).toEqual("Test Folder/Level 2 Folder"); + expect(result.collections[2].name).toEqual("Test Folder/Level 2 Folder/Level 3 Folder"); + + expect(result.collectionRelationships.length).toBe(4); + expect(result.collectionRelationships[0]).toEqual([1, 0]); + expect(result.collectionRelationships[1]).toEqual([2, 1]); + expect(result.collectionRelationships[2]).toEqual([3, 1]); + expect(result.collectionRelationships[3]).toEqual([4, 2]); + }); }); diff --git a/libs/importer/src/importers/roboform-csv-importer.spec.ts b/libs/importer/src/importers/roboform-csv-importer.spec.ts index dd385e10b8d..23604042a02 100644 --- a/libs/importer/src/importers/roboform-csv-importer.spec.ts +++ b/libs/importer/src/importers/roboform-csv-importer.spec.ts @@ -2,7 +2,7 @@ import { CipherType } from "@bitwarden/common/vault/enums"; import { RoboFormCsvImporter } from "./roboform-csv-importer"; import { data as dataNoFolder } from "./spec-data/roboform-csv/empty-folders"; -import { data as dataFolder } from "./spec-data/roboform-csv/with-folders"; +import { data as dataFolder, dataWithFolderHierarchy } from "./spec-data/roboform-csv/with-folders"; describe("Roboform CSV Importer", () => { beforeEach(() => { @@ -39,4 +39,19 @@ describe("Roboform CSV Importer", () => { expect(result.ciphers[4].notes).toBe("This is a safe note"); expect(result.ciphers[4].name).toBe("note - 2023-03-31"); }); + + it("should parse CSV data with folder hierarchy", async () => { + const importer = new RoboFormCsvImporter(); + const result = await importer.parse(dataWithFolderHierarchy); + expect(result != null).toBe(true); + + expect(result.folders.length).toBe(5); + expect(result.ciphers.length).toBe(5); + + expect(result.folders[0].name).toBe("folder1"); + expect(result.folders[1].name).toBe("folder2"); + expect(result.folders[2].name).toBe("folder2/folder3"); + expect(result.folders[3].name).toBe("folder1/folder2/folder3"); + expect(result.folders[4].name).toBe("folder1/folder2"); + }); }); diff --git a/libs/importer/src/importers/spec-data/keeper-csv/testdata.csv.ts b/libs/importer/src/importers/spec-data/keeper-csv/testdata.csv.ts index a40e97ff3fd..cfa51faecc6 100644 --- a/libs/importer/src/importers/spec-data/keeper-csv/testdata.csv.ts +++ b/libs/importer/src/importers/spec-data/keeper-csv/testdata.csv.ts @@ -2,3 +2,9 @@ export const testData = `"Foo","Bar","john.doe@example.com","1234567890abcdef"," "Foo","Bar 1","john.doe1@example.com","234567890abcdef1","https://an.example.com/","","","Account ID","12345","Org ID","54321" "Foo\\Baz","Bar 2","john.doe2@example.com","34567890abcdef12","https://another.example.com/","","","Account ID","23456","TFC:Keeper","otpauth://totp/Amazon:me@company.com?secret=JBSWY3DPEHPK3PXP&issuer=Amazon&algorithm=SHA1&digits=6&period=30" `; + +export const testDataMultiCollection = ` +"Foo\\Baz\\Bar","Bar 2","john.doe2@example.com","34567890abcdef12","https://another.example.com/","","","Account ID","23456","TFC:Keeper","otpauth://totp/Amazon:me@company.com?secret=JBSWY3DPEHPK3PXP&issuer=Amazon&algorithm=SHA1&digits=6&period=30" +"Foo\\Baz","Bar 2","john.doe2@example.com","34567890abcdef12","https://another.example.com/","","","Account ID","23456","TFC:Keeper","otpauth://totp/Amazon:me@company.com?secret=JBSWY3DPEHPK3PXP&issuer=Amazon&algorithm=SHA1&digits=6&period=30" +"Foo","Bar 2","john.doe2@example.com","34567890abcdef12","https://another.example.com/","","","Account ID","23456","TFC:Keeper","otpauth://totp/Amazon:me@company.com?secret=JBSWY3DPEHPK3PXP&issuer=Amazon&algorithm=SHA1&digits=6&period=30" +`; diff --git a/libs/importer/src/importers/spec-data/netwrix-csv/login-export.csv.ts b/libs/importer/src/importers/spec-data/netwrix-csv/login-export.csv.ts index 715dd8e0074..5b0fa0c5cb2 100644 --- a/libs/importer/src/importers/spec-data/netwrix-csv/login-export.csv.ts +++ b/libs/importer/src/importers/spec-data/netwrix-csv/login-export.csv.ts @@ -2,3 +2,6 @@ "folderOrCollection1";"tag1, tag2, tag3";"Test Entry 1";"someUser";"somePassword";"https://www.example.com";"some note for example.com";"someTOTPSeed" "folderOrCollection2";"tag2";"Test Entry 2";"jdoe";"})9+Kg2fz_O#W1§H1-0Zio";"www.123.com";"Description123";"anotherTOTP" "folderOrCollection1";"someTag";"Test Entry 3";"username";"password";"www.internetsite.com";"Information";""`; + +export const credentialsDataWithFolders = `"Organisationseinheit";"DataTags";"Beschreibung";"Benutzername";"Passwort";"Internetseite";"Informationen";"One-Time Passwort" +"folder1\\folder2\\folder3";"tag1, tag2, tag3";"Test Entry 1";"someUser";"somePassword";"https://www.example.com";"some note for example.com";"someTOTPSeed"`; diff --git a/libs/importer/src/importers/spec-data/passwordxp-csv/passwordxp-with-folders.csv.ts b/libs/importer/src/importers/spec-data/passwordxp-csv/passwordxp-with-folders.csv.ts index c7cfe825759..884929cfc8f 100644 --- a/libs/importer/src/importers/spec-data/passwordxp-csv/passwordxp-with-folders.csv.ts +++ b/libs/importer/src/importers/spec-data/passwordxp-csv/passwordxp-with-folders.csv.ts @@ -11,3 +11,17 @@ test;testtest;;http://test;test;27-3-2024 12:36:59;27-3-2024 12:36:59;;; [Cert folder\\Nested folder]; test2;testtest;;http://test;test;27-3-2024 12:36:59;27-3-2024 12:36:59;;;`; + +export const withMultipleFolders = `Title;User name;Account;URL;Password;Modified;Created;Expire on;Description;Modified by +>>> +Title2;Username2;Account2;http://URL2.com;12345678;27-3-2024 08:11:21;27-3-2024 08:11:21;;; + +[Test Folder] +Title Test 1;Username1;Account1;http://URL1.com;Password1;27-3-2024 08:10:52;27-3-2024 08:10:52;;; + +[Test Folder\\Level 2 Folder] +Certificate 1;;;;;27-3-2024 10:22:39;27-3-2024 10:22:39;;; +test;testtest;;http://test;test;27-3-2024 12:36:59;27-3-2024 12:36:59;;; + +[Test Folder\\Level 2 Folder\\Level 3 Folder] +test2;testtest;;http://test;test;27-3-2024 12:36:59;27-3-2024 12:36:59;;;`; diff --git a/libs/importer/src/importers/spec-data/roboform-csv/with-folders.ts b/libs/importer/src/importers/spec-data/roboform-csv/with-folders.ts index e836c6430f0..86757b79c86 100644 --- a/libs/importer/src/importers/spec-data/roboform-csv/with-folders.ts +++ b/libs/importer/src/importers/spec-data/roboform-csv/with-folders.ts @@ -4,3 +4,10 @@ Test,https://www.test.com/,https://www.test.com/,test@gmail.com,:testPassword,te LoginWebsite,https://login.Website.com/,https://login.Website.com/,test@outlook.com,123password,,folder2,"User ID$,,,txt,test@outlook.com","Password$,,,pwd,123password" Website,https://signin.website.com/,https://signin.website.com/,user@bitwarden.com,password123,Website ,folder3,"User ID$,,,txt,user@bitwarden.com","Password$,,,pwd,password123" note - 2023-03-31,,,,,This is a safe note,`; + +export const dataWithFolderHierarchy = `Name,Url,MatchUrl,Login,Pwd,Note,Folder,RfFieldsV2 +Bitwarden,https://bitwarden.com,https://bitwarden.com,user@bitwarden.com,password,,folder1,"User ID$,,,txt,user@bitwarden.com","Password$,,,pwd,password" +Test,https://www.test.com/,https://www.test.com/,test@gmail.com,:testPassword,test,folder1,"User ID$,,,txt,test@gmail.com","Password$,,,pwd,:testPassword" +LoginWebsite,https://login.Website.com/,https://login.Website.com/,test@outlook.com,123password,,folder2,"User ID$,,,txt,test@outlook.com","Password$,,,pwd,123password" +Website,https://signin.website.com/,https://signin.website.com/,user@bitwarden.com,password123,Website ,folder2\\folder3,"User ID$,,,txt,user@bitwarden.com","Password$,,,pwd,password123" +Website,https://signin.website.com/,https://signin.website.com/,user@bitwarden.com,password123,Website ,folder1\\folder2\\folder3,"User ID$,,,txt,user@bitwarden.com","Password$,,,pwd,password123"`; From 5b43be780b9085ed3e4094de22d727f7fb97f720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lison=20Fernandes?= Date: Mon, 14 Apr 2025 16:56:09 +0100 Subject: [PATCH 10/47] Remove duplicated copy (#14271) --- .github/DISCUSSION_TEMPLATE/password-manager.yml | 2 +- .github/DISCUSSION_TEMPLATE/secrets-manager.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/DISCUSSION_TEMPLATE/password-manager.yml b/.github/DISCUSSION_TEMPLATE/password-manager.yml index bc3938c1962..1d464ca9504 100644 --- a/.github/DISCUSSION_TEMPLATE/password-manager.yml +++ b/.github/DISCUSSION_TEMPLATE/password-manager.yml @@ -3,7 +3,7 @@ body: - type: markdown attributes: value: | - If you would like to contribute code to the Bitwarden codebase for consideration, please review [https://contributing.bitwarden.com/](https://contributing.bitwarden.com/) before posting. To keep discussion on topic, posts that do not include a proposal for a code contribution you wish to develop will be removed. For feature requests and community discussion, please visit https://community.bitwarden.com/ + If you would like to contribute code to the Bitwarden codebase for consideration, please review [https://contributing.bitwarden.com/](https://contributing.bitwarden.com/) before posting. To keep discussion on topic, posts that do not include a proposal for a code contribution you wish to develop will be removed. - type: dropdown attributes: label: Select Topic Area diff --git a/.github/DISCUSSION_TEMPLATE/secrets-manager.yml b/.github/DISCUSSION_TEMPLATE/secrets-manager.yml index bc3938c1962..1d464ca9504 100644 --- a/.github/DISCUSSION_TEMPLATE/secrets-manager.yml +++ b/.github/DISCUSSION_TEMPLATE/secrets-manager.yml @@ -3,7 +3,7 @@ body: - type: markdown attributes: value: | - If you would like to contribute code to the Bitwarden codebase for consideration, please review [https://contributing.bitwarden.com/](https://contributing.bitwarden.com/) before posting. To keep discussion on topic, posts that do not include a proposal for a code contribution you wish to develop will be removed. For feature requests and community discussion, please visit https://community.bitwarden.com/ + If you would like to contribute code to the Bitwarden codebase for consideration, please review [https://contributing.bitwarden.com/](https://contributing.bitwarden.com/) before posting. To keep discussion on topic, posts that do not include a proposal for a code contribution you wish to develop will be removed. - type: dropdown attributes: label: Select Topic Area From ac1210a7ed1ead4c786cac19e172367b1ae9b704 Mon Sep 17 00:00:00 2001 From: Bryan Cunningham Date: Mon, 14 Apr 2025 12:56:30 -0400 Subject: [PATCH 11/47] remove margin from checkbox hint (#14251) --- libs/components/src/form-control/form-control.component.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libs/components/src/form-control/form-control.component.html b/libs/components/src/form-control/form-control.component.html index b15202b0223..cc9c3dabbb6 100644 --- a/libs/components/src/form-control/form-control.component.html +++ b/libs/components/src/form-control/form-control.component.html @@ -14,7 +14,9 @@ } @if (!hasError) { - + + + } From 95ea1b22aea9389ee2853237d82f761268a2928a Mon Sep 17 00:00:00 2001 From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Date: Mon, 14 Apr 2025 12:47:09 -0500 Subject: [PATCH 12/47] [PM-17987] Add feature flag (#13991) * Add feature flag * Add unit tests. --- libs/common/src/enums/feature-flag.enum.ts | 2 + .../encrypt.service.implementation.ts | 21 +++++- .../crypto/services/encrypt.service.spec.ts | 70 ++++++++++++++++++- 3 files changed, 90 insertions(+), 3 deletions(-) diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 09708859ac8..fa776285ead 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -39,6 +39,7 @@ export enum FeatureFlag { PrivateKeyRegeneration = "pm-12241-private-key-regeneration", UserKeyRotationV2 = "userkey-rotation-v2", PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service", + PM17987_BlockType0 = "pm-17987-block-type-0", /* Tools */ ItemShare = "item-share", @@ -121,6 +122,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.PrivateKeyRegeneration]: FALSE, [FeatureFlag.UserKeyRotationV2]: FALSE, [FeatureFlag.PM4154_BulkEncryptionService]: FALSE, + [FeatureFlag.PM17987_BlockType0]: FALSE, /* Platform */ [FeatureFlag.IpcChannelFramework]: FALSE, diff --git a/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts index d10061a2be8..10d29198ada 100644 --- a/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts @@ -19,10 +19,17 @@ import { SymmetricCryptoKey, } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { + DefaultFeatureFlagValue, + FeatureFlag, + getFeatureFlagValue, +} from "../../../enums/feature-flag.enum"; import { ServerConfig } from "../../../platform/abstractions/config/server-config"; import { EncryptService } from "../abstractions/encrypt.service"; export class EncryptServiceImplementation implements EncryptService { + private blockType0: boolean = DefaultFeatureFlagValue[FeatureFlag.PM17987_BlockType0]; + constructor( protected cryptoFunctionService: CryptoFunctionService, protected logService: LogService, @@ -31,7 +38,7 @@ export class EncryptServiceImplementation implements EncryptService { // Handle updating private properties to turn on/off feature flags. onServerConfigChange(newConfig: ServerConfig): void { - return; + this.blockType0 = getFeatureFlagValue(newConfig, FeatureFlag.PM17987_BlockType0); } async encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise { @@ -39,6 +46,12 @@ export class EncryptServiceImplementation implements EncryptService { throw new Error("No encryption key provided."); } + if (this.blockType0) { + if (key.encType === EncryptionType.AesCbc256_B64 || key.key.byteLength < 64) { + throw new Error("Type 0 encryption is not supported."); + } + } + if (plainValue == null) { return Promise.resolve(null); } @@ -70,6 +83,12 @@ export class EncryptServiceImplementation implements EncryptService { throw new Error("No encryption key provided."); } + if (this.blockType0) { + if (key.encType === EncryptionType.AesCbc256_B64 || key.key.byteLength < 64) { + throw new Error("Type 0 encryption is not supported."); + } + } + const innerKey = key.inner(); if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) { const encValue = await this.aesEncrypt(plainValue, innerKey); diff --git a/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts b/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts index 275fd266f84..c65c78d88d7 100644 --- a/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts @@ -10,6 +10,8 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym import { CsprngArray } from "@bitwarden/common/types/csprng"; import { makeStaticByteArray } from "../../../../spec"; +import { DefaultFeatureFlagValue, FeatureFlag } from "../../../enums/feature-flag.enum"; +import { ServerConfig } from "../../../platform/abstractions/config/server-config"; import { EncryptServiceImplementation } from "./encrypt.service.implementation"; @@ -26,17 +28,65 @@ describe("EncryptService", () => { encryptService = new EncryptServiceImplementation(cryptoFunctionService, logService, true); }); + describe("onServerConfigChange", () => { + const newConfig = mock(); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it("updates internal flag with default value when not present in config", () => { + encryptService.onServerConfigChange(newConfig); + + expect((encryptService as any).blockType0).toBe( + DefaultFeatureFlagValue[FeatureFlag.PM17987_BlockType0], + ); + }); + + test.each([true, false])("updates internal flag with value in config", (expectedValue) => { + newConfig.featureStates = { [FeatureFlag.PM17987_BlockType0]: expectedValue }; + + encryptService.onServerConfigChange(newConfig); + + expect((encryptService as any).blockType0).toBe(expectedValue); + }); + }); + describe("encrypt", () => { it("throws if no key is provided", () => { return expect(encryptService.encrypt(null, null)).rejects.toThrow( "No encryption key provided.", ); }); - it("returns null if no data is provided", async () => { - const key = mock(); + + it("throws if type 0 key is provided with flag turned on", async () => { + (encryptService as any).blockType0 = true; + const key = new SymmetricCryptoKey(makeStaticByteArray(32)); + const mock32Key = mock(); + mock32Key.key = makeStaticByteArray(32); + + await expect(encryptService.encrypt(null!, key)).rejects.toThrow( + "Type 0 encryption is not supported.", + ); + await expect(encryptService.encrypt(null!, mock32Key)).rejects.toThrow( + "Type 0 encryption is not supported.", + ); + + const plainValue = "data"; + await expect(encryptService.encrypt(plainValue, key)).rejects.toThrow( + "Type 0 encryption is not supported.", + ); + await expect(encryptService.encrypt(plainValue, mock32Key)).rejects.toThrow( + "Type 0 encryption is not supported.", + ); + }); + + it("returns null if no data is provided with valid key", async () => { + const key = new SymmetricCryptoKey(makeStaticByteArray(64)); const actual = await encryptService.encrypt(null, key); expect(actual).toBeNull(); }); + it("creates an EncString for Aes256Cbc", async () => { const key = new SymmetricCryptoKey(makeStaticByteArray(32)); const plainValue = "data"; @@ -53,6 +103,7 @@ describe("EncryptService", () => { expect(Utils.fromB64ToArray(result.data).length).toEqual(4); expect(Utils.fromB64ToArray(result.iv).length).toEqual(16); }); + it("creates an EncString for Aes256Cbc_HmacSha256_B64", async () => { const key = new SymmetricCryptoKey(makeStaticByteArray(64)); const plainValue = "data"; @@ -90,6 +141,21 @@ describe("EncryptService", () => { ); }); + it("throws if type 0 key provided with flag turned on", async () => { + (encryptService as any).blockType0 = true; + const key = new SymmetricCryptoKey(makeStaticByteArray(32)); + const mock32Key = mock(); + mock32Key.key = makeStaticByteArray(32); + + await expect(encryptService.encryptToBytes(plainValue, key)).rejects.toThrow( + "Type 0 encryption is not supported.", + ); + + await expect(encryptService.encryptToBytes(plainValue, mock32Key)).rejects.toThrow( + "Type 0 encryption is not supported.", + ); + }); + it("encrypts data with provided Aes256Cbc key and returns correct encbuffer", async () => { const key = new SymmetricCryptoKey(makeStaticByteArray(32, 0)); const iv = makeStaticByteArray(16, 80); From 356a20a4bce174cecf98ee6545b569bc2439414e Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Mon, 14 Apr 2025 15:55:01 -0400 Subject: [PATCH 13/47] fix(login): [PM-20174] Do not show validation errors on email input on LoginComponent * Do not show validation errors on input * Removed one-line function. * Removed awaits --- .../auth/src/angular/login/login.component.ts | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts index 55c282be55c..8a198663e06 100644 --- a/libs/auth/src/angular/login/login.component.ts +++ b/libs/auth/src/angular/login/login.component.ts @@ -400,12 +400,6 @@ export class LoginComponent implements OnInit, OnDestroy { await this.router.navigate(["/login-with-device"]); } - protected async emailIsValid(): Promise { - this.formGroup.controls.email.markAsTouched(); - this.formGroup.controls.email.updateValueAndValidity({ onlySelf: true, emitEvent: true }); - return this.formGroup.controls.email.valid; - } - protected async toggleLoginUiState(value: LoginUiState): Promise { this.loginUiState = value; @@ -474,7 +468,7 @@ export class LoginComponent implements OnInit, OnDestroy { * Continue to the master password entry state (only if email is validated) */ protected async continue(): Promise { - const isEmailValid = await this.emailIsValid(); + const isEmailValid = this.validateEmail(); if (isEmailValid) { await this.toggleLoginUiState(LoginUiState.MASTER_PASSWORD_ENTRY); @@ -496,7 +490,7 @@ export class LoginComponent implements OnInit, OnDestroy { */ async handleSsoClick() { // Make sure the email is valid - const isEmailValid = await this.emailIsValid(); + const isEmailValid = this.validateEmail(); if (!isEmailValid) { return; } @@ -594,11 +588,21 @@ export class LoginComponent implements OnInit, OnDestroy { } }; + /** + * Validates the email and displays any validation errors. + * @returns true if the email is valid, false otherwise. + */ + protected validateEmail(): boolean { + this.formGroup.controls.email.markAsTouched(); + this.formGroup.controls.email.updateValueAndValidity({ onlySelf: true, emitEvent: true }); + return this.formGroup.controls.email.valid; + } + /** * Persist the entered email address and the user's choice to remember it to state. */ private async persistEmailIfValid(): Promise { - if (await this.emailIsValid()) { + if (this.formGroup.controls.email.valid) { const email = this.formGroup.value.email; const rememberEmail = this.formGroup.value.rememberEmail ?? false; if (!email) { @@ -613,7 +617,7 @@ export class LoginComponent implements OnInit, OnDestroy { } /** - * Set the email value from the input field. + * Set the email value from the input field and persists to state if valid. * We only update the form controls onSubmit instead of onBlur because we don't want to show validation errors until * the user submits. This is because currently our validation errors are shown below the input fields, and * displaying them causes the screen to "jump". @@ -626,7 +630,7 @@ export class LoginComponent implements OnInit, OnDestroy { } /** - * Set the Remember Email value from the input field. + * Set the Remember Email value from the input field and persists to state if valid. * We only update the form controls onSubmit instead of onBlur because we don't want to show validation errors until * the user submits. This is because currently our validation errors are shown below the input fields, and * displaying them causes the screen to "jump". From 938e9454e140668d51dd03e81fac58c0a9c53d3a Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Mon, 14 Apr 2025 21:33:51 -0400 Subject: [PATCH 14/47] fix(workflow): [PM-19254] Update image tag generation for builds from forked PRs * Added fork name to tag * Added logging. * Added pull_request_target * Added repository name if on fork. * Limited characters * Added sanitization * Moved to env var for extra security. --- .github/workflows/build-web.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index 12748a47748..3da524702fe 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -45,7 +45,7 @@ on: env: _AZ_REGISTRY: bitwardenprod.azurecr.io - + _GITHUB_PR_REPO_NAME: ${{ github.event.pull_request.head.repo.full_name }} jobs: setup: @@ -190,12 +190,18 @@ jobs: - name: Generate container image tag id: tag run: | - if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then - IMAGE_TAG=$(echo "${GITHUB_HEAD_REF}" | sed "s#/#-#g") + if [[ "${GITHUB_EVENT_NAME}" == "pull_request" || "${GITHUB_EVENT_NAME}" == "pull_request_target" ]]; then + IMAGE_TAG=$(echo "${GITHUB_HEAD_REF}" | sed "s/[^a-zA-Z0-9]/-/g") # Sanitize branch name to alphanumeric only else IMAGE_TAG=$(echo "${GITHUB_REF_NAME}" | sed "s#/#-#g") fi + if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then + SANITIZED_REPO_NAME=$(echo "$_GITHUB_PR_REPO_NAME" | sed "s/[^a-zA-Z0-9]/-/g") # Sanitize repo name to alphanumeric only + IMAGE_TAG=$SANITIZED_REPO_NAME-$IMAGE_TAG # Add repo name to the tag + IMAGE_TAG=${IMAGE_TAG:0:128} # Limit to 128 characters, as that's the max length for Docker image tags + fi + if [[ "$IMAGE_TAG" == "main" ]]; then IMAGE_TAG=dev fi From eb9812091cc376dc38cb33339d82d09f0f482521 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Tue, 15 Apr 2025 11:59:48 +0200 Subject: [PATCH 15/47] Autosync the updated translations (#14234) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/src/locales/af/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/ar/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/az/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/be/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/bg/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/bn/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/bs/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/ca/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/cs/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/cy/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/da/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/de/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/el/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/en_GB/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/en_IN/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/eo/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/es/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/et/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/eu/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/fa/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/fi/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/fil/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/fr/messages.json | 78 +++++++++++++++++++++- apps/web/src/locales/gl/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/he/messages.json | 74 ++++++++++++++++++++- apps/web/src/locales/hi/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/hr/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/hu/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/id/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/it/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/ja/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/ka/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/km/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/kn/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/ko/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/lv/messages.json | 76 +++++++++++++++++++++- apps/web/src/locales/ml/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/mr/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/my/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/nb/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/ne/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/nl/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/nn/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/or/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/pl/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/pt_BR/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/pt_PT/messages.json | 76 +++++++++++++++++++++- apps/web/src/locales/ro/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/ru/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/si/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/sk/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/sl/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/sr/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/sr_CS/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/sv/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/te/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/th/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/tr/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/uk/messages.json | 74 ++++++++++++++++++++- apps/web/src/locales/vi/messages.json | 72 +++++++++++++++++++++ apps/web/src/locales/zh_CN/messages.json | 82 ++++++++++++++++++++++-- apps/web/src/locales/zh_TW/messages.json | 72 +++++++++++++++++++++ 62 files changed, 4478 insertions(+), 14 deletions(-) diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index 29eb76a1069..56eabc34863 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "U kan die eksterne ID as verwysing gebruik of om hierdie hulpbron aan ’n eksterne stelsel soos ’n gebruikersgids te koppel." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Nest collection under" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn is suksesvol bevestig! U kan hierdie oortjie sluit." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "U wagwoordwenk kan nie dieselfde as u wagwoord wees nie." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Vingerafdruk" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Verwyder gebruikers" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index f12f59b8fba..9ebd9873655 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "External ID is an unencrypted reference used by the Bitwarden Directory Connector and API." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Nest collection under" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn verified successfully! You may close this tab." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Fingerprint" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Remove users" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index 069986361ca..0117fa37ae4 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "Xarici kimlik, referans olaraq və ya bu mənbəni istifadəçi kataloqu kimi xarici bir sistemə bağlamaq üçün istifadə edilir." }, + "ssoExternalId": { + "message": "SSO Xarici Kimlik" + }, + "ssoExternalIdDesc": { + "message": "SSO Xarici Kimlik, Bitwarden və konfiqurasiya edilmiş SSO provayderiniz arasında şifrələnməmiş referansdır." + }, "nestCollectionUnder": { "message": "Kolleksiyanı bunun altında yerləşdir" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn kimliyi doğrulaması uğurlu oldu! Bu vərəqi bağlaya bilərsiniz." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Yeni parolunuz hazırkı parolunuzla eyni ola bilməz." + }, "hintEqualsPassword": { "message": "Parol məsləhəti, parolunuzla eyni ola bilməz." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Barmaq izi" }, + "fingerprintPhrase": { + "message": "Barmaq izi ifadəsi:" + }, "removeUsers": { "message": "İstifadəçiləri çıxart" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Təşkilat adı 50 xarakterdən çox ola bilməz." }, + "rotationCompletedTitle": { + "message": "Açar döndərmə uğurludur" + }, + "rotationCompletedDesc": { + "message": "Ana parol və şifrələmə açarınız güncəllənib. Digər cihazlarınızda hesabdan çıxış edildi." + }, + "trustUserEmergencyAccess": { + "message": "İstifadəçiyə güvən və təsdiqlə" + }, + "trustOrganization": { + "message": "Təşkilata güvən" + }, + "trust": { + "message": "Güvən" + }, + "doNotTrust": { + "message": "Güvənmə" + }, + "emergencyAccessTrustWarning": { + "message": "Hesabınızın təhlükəsizliyi üçün yalnız bu istifadəçiyə fövqəladə hal müraciəti icazəsini verdiyinizi və onun barmaq izinin hesabında görünən barmaq izi ilə uyuşduğunu təsdiqləyin" + }, + "orgTrustWarning": { + "message": "Hesabınızın təhlükəsizliyi üçün yalnız bu təşkilatın üzvüsünüzsə, hesab geri qaytarma fəaldırsa və aşağıda görünən barmaq izi təşkilatın barmaq izi ilə uyuşursa davam edin." + }, + "trustUser": { + "message": "İstifadəçiyə güvən" + }, "sshKeyWrongPassword": { "message": "Daxil etdiyiniz parol yanlışdır." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Təyin edilmiş yer sayı, boş yer sayından çoxdur." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fövqəladə hal müraciətini fəallaşdırdığınız $NUM_USERS$ kontakt üçün barmaq izi ifadəsi.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Hesab geri qaytarma seçimini fəallaşdırdığınız $ORG_NAME$ təşkilatı üçün barmaq izi ifadəsi.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Şifrələmə açarlarını döndərmək, sizdən hesabınızı geri qaytara biləcək istənilən təşkilatın açarlarına və fövqəladə hal müraciətini fəallaşdırdığınız istənilən kontakta güvənməyinizi tələb edəcək. Davam etmək üçün əmin olun ki, aşağıdakıları doğrulaya bilirsiniz:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Güvənilməyən şifrələmə açarları" + }, "changeAtRiskPassword": { "message": "Riskli parolları dəyişdir" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Ödənişsiz təşkilatların ən çox 2 kolleksiyası ola bilər. Daha çox kolleksiya əlavə etmək üçün ödənişli bir plana yüksəldin." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index 9bb0336777a..0b0b405721f 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "Знешні ідэнтыфікатар можа быць выкарыстаны ў якасці спасылкі для сувязі гэтага рэсурсу са знешняй сістэмай, такой як каталог карыстальніка." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Размясціць калекцыю пад" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn паспяхова правераны! Вы можаце закрыць гэту ўкладку." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Падказка для пароля не можа супадаць з паролем." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Адбітак" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Выдаліць карыстальнiкаў" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index 87396ddfcdc..de23b298710 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "Външните идентификатори указват връзката на този елемент с друга система, напр. директорийна услуга с потребители и групи." }, + "ssoExternalId": { + "message": "Външен ИД за еднократно удостоверяване" + }, + "ssoExternalIdDesc": { + "message": "Външният ИД за еднократно удостоверяване е нешифрована връзка между Битуорден и настроеният доставчик на еднократно удостоверяване." + }, "nestCollectionUnder": { "message": "Влагане на колекцията под" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "Успешно удостоверяване чрез WebAuthn!
Можете да затворите този раздел." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Новата парола не може да бъде същата като текущата." + }, "hintEqualsPassword": { "message": "Подсказването за паролата не може да съвпада с нея." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Отпечатък" }, + "fingerprintPhrase": { + "message": "Уникална фраза:" + }, "removeUsers": { "message": "Премахване на потребителите" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Името на организацията не може да бъде по-дълго от 50 знака." }, + "rotationCompletedTitle": { + "message": "Успешна промяна на ключа" + }, + "rotationCompletedDesc": { + "message": "Вашата главна парола и шифриращи ключове бяха променени. Бяхте отписан(а) на другите си устройства." + }, + "trustUserEmergencyAccess": { + "message": "Даване на доверие и потвърждаване на потребителя" + }, + "trustOrganization": { + "message": "Даване на доверие на организацията" + }, + "trust": { + "message": "Даване на доверие" + }, + "doNotTrust": { + "message": "Да не се дава доверие" + }, + "emergencyAccessTrustWarning": { + "message": "С оглед на сигурността на акаунта Ви, потвърдете само, ако сте дали на този потребител достъп за спешни случаи и ако отпечатъкът му съвпада с това, което се вижда в акаунта му" + }, + "orgTrustWarning": { + "message": "С оглед на сигурността на акаунта Ви, продължете само, ако сте член на тази организация, ако възстановяването на акаунта Ви е включено и ако отпечатъкът показан по-долу съвпада с този на организацията." + }, + "trustUser": { + "message": "Даване на доверие на потребителя" + }, "sshKeyWrongPassword": { "message": "Въведената парола е неправилна." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Назначените места превишават наличния брой." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Уникална фраза за $NUM_USERS$ контакти, за които сте включили достъпа за спешни случаи.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Уникална фраза за организацията $ORG_NAME$, за която сте включили възстановяването на акаунти.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Промяната на шифроващите ключове ще изисква да се доверите на ключовете на всяка организация, която може да възстанови акаунта Ви, и на всички контакти, на които сте дали достъп за спешни случаи. За да продължите, уверете се, че можете да потвърдите следното:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Недоверени шифроващи ключове" + }, "changeAtRiskPassword": { "message": "Промяна на парола в риск" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Безплатните организации могат да имат не повече от 2 колекции. Надградете до платен план, ако искате да имате повече колекции." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index 67b602fe256..dc879696b3c 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "External ID is an unencrypted reference used by the Bitwarden Directory Connector and API." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Nest collection under" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn verified successfully! You may close this tab." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Fingerprint" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Remove users" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index 58077510f1e..aea556cde36 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "External ID is an unencrypted reference used by the Bitwarden Directory Connector and API." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Nest collection under" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn verified successfully! You may close this tab." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Fingerprint" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Remove users" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index 6ea513b4490..93fa6e5793b 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "L'identificador extern es pot utilitzar com a referència o enllaçar aquest recurs a un sistema extern, com ara un directori d'usuari." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Anida col·lecció sota" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn s'ha verificat correctament! Podeu tancar aquesta pestanya." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "El vostre suggeriment de contrasenya no pot ser el mateix que la vostra contrasenya." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Empremta digital" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Suprimeix usuaris" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Unitat de negoci" + }, + "businessUnits": { + "message": "Unitats de negoci" + }, + "newBusinessUnit": { + "message": "Unitat de negoci nova" } } diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index fe431b3a1f0..35b0904bda2 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "Externí ID je nešifrovaná reference použitá adresářovým konektorem Bitwardenu a API." }, + "ssoExternalId": { + "message": "Externí SSO ID" + }, + "ssoExternalIdDesc": { + "message": "Externí SSO ID je nešifrovaná reference mezi Bitwardenem a Vaším nastaveným poskytovatelem SSO." + }, "nestCollectionUnder": { "message": "Vnořit kolekci pod" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn byl úspěšně ověřen! Můžete zavřít tuto kartu." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Vaše nové heslo nemůže být stejné jako Vaše současné heslo." + }, "hintEqualsPassword": { "message": "Nápověda k Vašemu heslu nemůže být stejná jako Vaše heslo." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Otisk prstu" }, + "fingerprintPhrase": { + "message": "Fráze otisku prstu:" + }, "removeUsers": { "message": "Odebrat uživatele" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Název organizace nesmí přesáhnout 50 znaků." }, + "rotationCompletedTitle": { + "message": "Úspěšná rotace klíče" + }, + "rotationCompletedDesc": { + "message": "Vaše hlavní heslo a šifrovací klíče byly aktualizovány. Vaše další zařízení byla odhlášena." + }, + "trustUserEmergencyAccess": { + "message": "Důvěřovat a potvrdit uživatele" + }, + "trustOrganization": { + "message": "Důvěřovat organizaci" + }, + "trust": { + "message": "Důvěřovat" + }, + "doNotTrust": { + "message": "Nedůvěřovat" + }, + "emergencyAccessTrustWarning": { + "message": "Pro zabezpečení Vašeho účtu potvrďte jen v případě, že jste tomuto uživateli udělili nouzový přístup a jeho otisk prstu odpovídá tomu, co je zobrazeno v jeho účtu." + }, + "orgTrustWarning": { + "message": "Pro zabezpečení Vašeho účtu pokračujte jen v případě, že jste členem této organizace, máte povoleno obnovení účtu a zobrazený otisk prstu níže odpovídá otisku prstu organizace." + }, + "trustUser": { + "message": "Důvěřovat uživateli" + }, "sshKeyWrongPassword": { "message": "Zadané heslo není správné." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Přiřazení uživatelé překračují dostupné uživatele." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fráze otisku prstu pro $NUM_USERS$ kontaktů, pro které jste povolili nouzový přístup.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fráze otisku prstu pro organizaci $ORG_NAME$ , pro kterou jste povolili obnovení účtu.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotace šifrovacích klíčů bude vyžadovat důvěru klíčů všech organizací, které mohou obnovit Váš účet, a všechny kontakty, pro které můžete povolit nouzový přístup. Chcete-li pokračovat, ujistěte se, že můžete ověřit následující:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Nedůvěryhodné šifrovací klíče" + }, "changeAtRiskPassword": { "message": "Změnit ohrožené heslo" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Bezplatné organizace mohou mít až 2 kolekce. Chcete-li přidat více kolekcí, přejděte na placený tarif." + }, + "businessUnit": { + "message": "Obchodní jednotka" + }, + "businessUnits": { + "message": "Obchodní jednotky" + }, + "newBusinessUnit": { + "message": "Nová obchodní jednotka" } } diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index 4fcd523f0db..69217c7f17b 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "External ID is an unencrypted reference used by the Bitwarden Directory Connector and API." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Nest collection under" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn verified successfully! You may close this tab." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Fingerprint" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Remove users" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index 525ed048c44..9999432c50d 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "Eksternt ID er en ikke-krypteret reference brugt af Bitwarden Directory Connector and API." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Placér samling under" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn bekræftet Du kan lukke denne fane." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Adgangskodetip og adgangskode må ikke være identiske." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Fingeraftryk" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Fjern brugere" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organisationsnavn må ikke overstige 50 tegn." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index edf253fac8b..a517629a5b4 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "Die externe ID ist eine unverschlüsselte Referenz, die vom Bitwarden Directory Connector und der API verwendet wird." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Sammlung verschachteln unter" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn erfolgreich verifiziert! Du kannst diesen Tab nun schließen." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Dein neues Passwort darf nicht dasselbe sein wie dein aktuelles Passwort." + }, "hintEqualsPassword": { "message": "Dein Passwort-Hinweis darf nicht identisch mit deinem Passwort sein." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Fingerabdruck" }, + "fingerprintPhrase": { + "message": "Fingerabdruck-Phrase:" + }, "removeUsers": { "message": "Benutzer entfernen" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Der Name der Organisation darf 50 Zeichen nicht überschreiten." }, + "rotationCompletedTitle": { + "message": "Schlüsselrotation erfolgreich" + }, + "rotationCompletedDesc": { + "message": "Dein Master-Passwort und Verschlüsselungsschlüssel wurden aktualisiert. Deine anderen Geräte wurden abgemeldet." + }, + "trustUserEmergencyAccess": { + "message": "Benutzer vertrauen und bestätigen" + }, + "trustOrganization": { + "message": "Organisation vertrauen" + }, + "trust": { + "message": "Vertrauen" + }, + "doNotTrust": { + "message": "Nicht vertrauen" + }, + "emergencyAccessTrustWarning": { + "message": "Bestätige zur Sicherheit deines Kontos nur, wenn du den Notfallzugriff diesem Benutzer gewährt hast und sein Fingerabdruck mit dem übereinstimmt, was in seinem Konto angezeigt wird" + }, + "orgTrustWarning": { + "message": "Fahre zur Sicherheit deines Kontos nur fort, wenn du ein Mitglied dieser Organisation bist, die Kontowiederherstellung aktiviert hast und der unten angezeigte Fingerabdruck mit dem Fingerabdruck der Organisation übereinstimmt." + }, + "trustUser": { + "message": "Benutzer vertrauen" + }, "sshKeyWrongPassword": { "message": "Dein eingegebenes Passwort ist falsch." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Die zugewiesenen Plätze überschreiten die verfügbaren Plätze." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerabdruck-Phrase für $NUM_USERS$ Kontakte, für die du den Notfallzugriff aktiviert hast.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerabdruck-Phrase für die Organisation $ORG_NAME$, für die du die Kontowiederherstellung aktiviert hast.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Die Rotation deiner Verschlüsselungsschlüssel erfordert, den Schlüsseln aller Organisationen, die dein Konto wiederherstellen können, sowie aller Kontakte, für die du den Notfallzugriff aktiviert hast, zu vertrauen. Um fortzufahren, stelle sicher, dass du Folgendes bestätigen kannst:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Nicht vertrauenswürdige Verschlüsselungsschlüssel" + }, "changeAtRiskPassword": { "message": "Gefährdetes Passwort ändern" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Kostenlose Organisationen können bis zu 2 Sammlungen haben. Upgrade auf ein kostenpflichtiges Abo, um mehr Sammlungen hinzuzufügen." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index a5a59f4257d..b44892b1276 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "Το εξωτερικό αναγνωριστικό id μπορεί να χρησιμοποιηθεί ως αναφορά ή να συνδέσει αυτόν τον πόρο με ένα εξωτερικό σύστημα, όπως έναν κατάλογο χρηστών." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Ένθεση συλλογής σε" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "Το WebAuthn επαληθεύτηκε επιτυχώς!
Μπορείτε να κλείσετε αυτή την καρτέλα." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Η υπόδειξη κωδικού πρόσβασης, δεν μπορεί να είναι η ίδια με τον κωδικό πρόσβασης σας." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Αποτύπωμα" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Αφαίρεση Χρηστών" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index 9f755b303bf..daef0bc6945 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "External ID is an unencrypted reference used by the Bitwarden Directory Connector and API." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Nest collection under" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn verified successfully! You may close this tab." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Fingerprint" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Remove users" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organisation name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organisation" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organisation, have account recovery enabled, and the fingerprint displayed below matches the organisation's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organisation $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organisations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organisations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index 1349e55f7b1..0804aa60e08 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "The external ID can be used as a reference or to link this resource to an external system such as a user directory." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Nest collection under" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn verified successfully! You may close this tab." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Fingerprint" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Remove Users" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organisation name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organisation" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organisation, have account recovery enabled, and the fingerprint displayed below matches the organisation's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organisation $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organisations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organisations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index 0cafbb8a8ef..33afe669868 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "La ekstera identigilo povas esti uzata kiel referenco aŭ por ligi ĉi tiun rimedon al ekstera sistemo kiel uzantdosierujo." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Nest collection under" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn verified successfully! You may close this tab." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Fingerprint" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Remove users" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index 338cc195c0d..d3f9c608748 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "El Id externo puede ser usado como una referencia o para enlazar este recurso a un sistema externo, por ejemplo, un directorio de usuario." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Colección anidada bajo" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "¡WebAuthn verificado con éxito! Puede cerrar esta pestaña." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "La pista de su contraseña no puede ser la misma que la contraseña." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Huella digital" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Eliminar usuarios" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index 9c2c407d3e2..b5ed1f22f61 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "Välist Id-d kasutatakse viitena või näiteks selleks, et siduda need ressursid välise süsteemiga, nagu näites kasutaja kataloog." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Lisa kogumike jaotisesse" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn on edukalt kinnitatud! Võid selle vahelehe sulgeda." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Parooli vihje ei saa olla sama mis parool ise." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Sõrmejälg" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Kasutajate eemaldamine" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index e6576e40664..06a13bb6765 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "Kanpoko Id-a erreferentzia gisa erabil daiteke edo baliabide hori kanpoko sistema batekin lotzeko, erabiltzaileen direktorio gisa adibidez." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Nest collection under" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn behar bezala egiaztatu da! Fitxa hau itxi dezakezu." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Zure pasahitza ezin da izan zure pasahitzaren pistaren berdina." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Hatz-marka" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Kendu erabiltzaileak" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index 143ac5f9c32..5e37f707f0c 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "شناسه خارجی می‌تواند به عنوان یک مرجع یا برای پیوند دادن این منبع به یک سیستم خارجی مانند فهرست کاربری استفاده شود." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "مجموعه لانه زیر" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn با موفقیت تأیید شد! می‌توانید این برگه را ببندید." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "اشاره به کلمه عبور شما نمی‌تواند همان کلمه عبور شما باشد." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "اثر انگشت" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "حذف کاربرها" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index 9612c8cb4ad..b5b5108aa14 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "Ulkoinen ID on Bitwarden Directory Connectin ja API:n käyttämä salaamaton viite." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Sijoita kokoelma seuraavan alle:" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn-todennus onnistui! Voit sulkea välilehden." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Salasanavihjeesi ei voi olla sama kuin salasanasi." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Sormenjälki" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Poista käyttäjiä" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "Syöttämäsi salasana on virheellinen." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Vaihda vaarantunut salasana" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index a6b82cc1902..04f97960ccf 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "Ang Panlabas na ID ay isang hindi naka encrypt na sanggunian na ginagamit ng Bitwarden Directory Connector at API." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Nest koleksyon sa ilalim ng" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "Matagumpay na na-verify ng WebAuthn! Maaari mong isara ang tab na ito." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Hindi pwedeng maging pareho ang password hint at password mo." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Fingerprint" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Tanggalin ang mga gumagamit" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index df113c1e988..0f6969aa333 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -1882,7 +1882,7 @@ "message": "Procédez ci-dessous pour que Bitwarden vous envoie des courriels de vérification lorsque vous vous connectez à partir d'un nouvel appareil." }, "turnOffNewDeviceLoginProtectionWarning": { - "message": "Avec la protection de connexion d'un nouvel appareil désactivée, toute personne ayant votre mot de passe maître peut accéder à votre compte depuis n'importe quel appareil. Pour protéger votre compte sans courriel de vérification, configurez la connexion en deux étapes." + "message": "Avec la protection de connexion d'un nouvel appareil désactivée, toute personne ayant votre mot de passe principal peut accéder à votre compte depuis n'importe quel appareil. Pour protéger votre compte sans courriel de vérification, configurez la connexion en deux étapes." }, "accountNewDeviceLoginProtectionSaved": { "message": "Modifications de la protection de connexion de l'appareil enregistrées" @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "L’identifiant externe peut être utilisé comme référence ou pour lier cette ressource à un système externe tel qu’un répertoire utilisateur." }, + "ssoExternalId": { + "message": "ID externe SSO" + }, + "ssoExternalIdDesc": { + "message": "ID externe SSO est une référence non chiffrée entre Bitwarden et votre fournisseur SSO configuré." + }, "nestCollectionUnder": { "message": "Collection imbriquée sous" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn vérifié avec succès ! Vous pouvez fermer cet onglet." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Votre nouveau mot de passe ne peut être le même que votre mot de passe actuel." + }, "hintEqualsPassword": { "message": "Votre indice de mot de passe ne peut pas être identique à votre mot de passe." }, @@ -5722,7 +5731,7 @@ } }, "eventAdminPasswordReset": { - "message": "Réinitialisation du mot de passe maître pour l'utilisateur $ID$.", + "message": "Réinitialisation du mot de passe principal pour l'utilisateur $ID$.", "placeholders": { "id": { "content": "$1", @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Empreinte" }, + "fingerprintPhrase": { + "message": "Phrase d'empreinte :" + }, "removeUsers": { "message": "Supprimer les utilisateurs" }, @@ -8388,7 +8400,7 @@ "message": "Appareils de confiance" }, "memberDecryptionOptionTdeDescPart1": { - "message": "Les membres n'auront pas besoin d'un mot de passe maître lors de la connexion avec SSO. Le mot de passe principal est remplacé par une clé de chiffrement stockée sur le périphérique, ce qui rend ce périphérique fiable. Le premier appareil avec lequel un membre créera son compte et se connectera sera fiable. Les nouveaux appareils devront être approuvés par un périphérique de confiance existant ou par un administrateur. La", + "message": "Les membres n'auront pas besoin d'un mot de passe principale lors de la connexion avec SSO. Le mot de passe principal est remplacé par une clé de chiffrement stockée sur le périphérique, ce qui rend ce périphérique fiable. Le premier appareil avec lequel un membre créera son compte et se connectera sera fiable. Les nouveaux appareils devront être approuvés par un périphérique de confiance existant ou par un administrateur. La", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Members will not need a master password when logging in with SSO. Master password is replaced with an encryption key stored on the device, making that device trusted. The first device a member creates their account and logs into will be trusted. New devices will need to be approved by an existing trusted device or by an administrator. The single organization policy, SSO required policy, and account recovery administration policy will turn on when this option is used.'" }, "memberDecryptionOptionTdeDescLink1": { @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Le nom de l'organisation ne doit pas dépasser 50 caractères." }, + "rotationCompletedTitle": { + "message": "Rotation de la clé réussie" + }, + "rotationCompletedDesc": { + "message": "Votre mot de passe principal et vos clés de chiffrement ont été mis à jour. Vos autres appareils ont été déconnectés." + }, + "trustUserEmergencyAccess": { + "message": "Faire confiance et confirmer l'utilisateur" + }, + "trustOrganization": { + "message": "Faire confiance à l'organisation" + }, + "trust": { + "message": "Faire confiance" + }, + "doNotTrust": { + "message": "Ne pas faire confiance" + }, + "emergencyAccessTrustWarning": { + "message": "Pour la sécurité de votre compte, confirmez seulement si vous avez accordé l'accès d'urgence à cet utilisateur et que sa phrase d'empreinte correspond à ce qui est affiché dans son compte" + }, + "orgTrustWarning": { + "message": "Pour la sécurité de votre compte, continuez seulement si vous êtes un membre de cette organisation, avez la récupération de compte activée et que la phrase d'empreinte correspond à celle de l'organisation." + }, + "trustUser": { + "message": "Faire confiance à l'utilisateur" + }, "sshKeyWrongPassword": { "message": "Le mot de passe que vous avez entré est incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Les places assignées dépassent les places disponibles." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Phrase d'empreinte des $NUM_USERS$ contacts pour lesquels vous avez activé l'accès d'urgence.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Phrase d'empreinte de l'organisation $ORG_NAME$ pour laquelle vous avez activé la récupération de compte.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Faire la rotation de vos clés exige que vous fassiez confiance aux clés de toutes les organisations qui peuvent récupérer votre compte et à tous les contacts à qui vous avez autorsé l'accès d'urgence. Pour continuer, assurez-vous que vous pouvez vérifier ce qui suit :" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Clés de chiffrement non fiables" + }, "changeAtRiskPassword": { "message": "Changer le mot de passe à risque" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Les organisations gratuites peuvent avoir jusqu'à 2 collections. Passez à une offre payante pour ajouter plus de collections." + }, + "businessUnit": { + "message": "Unité d'affaires" + }, + "businessUnits": { + "message": "Unités d’affaires" + }, + "newBusinessUnit": { + "message": "Nouvelle unité d'affaires" } } diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index c7a62c4fd60..33aec61c9ad 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "External ID is an unencrypted reference used by the Bitwarden Directory Connector and API." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Nest collection under" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn verified successfully! You may close this tab." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Fingerprint" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Remove users" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index 13cecb8f4dc..ee7f464b9fd 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "מזהה חיצוני הוא הפניה לא מוצפנת בשימוש על ידי מחבר הספריות וה־API של Bitwarden." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "לקנן אוסף תחת" }, @@ -3971,7 +3977,7 @@ "message": "כדי לערוך את כתובת הדוא\"ל שלך." }, "view": { - "message": "צפה" + "message": "הצג" }, "invalidDateRange": { "message": "טווח תאריכים לא חוקי." @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn אומת בהצלחה! אתה רשאי לסגור כרטיסיה זו." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "רמז הסיסמה שלך לא יכול להיות אותו הדבר כמו הסיסמה שלך." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "טביעת אצבע" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "הסר משתמשים" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "שם ארגון לא יכול לחרוג מ־50 תווים." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "הסיסמה שהזנת שגויה." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "מקומות מוקצים עולים על מקומות פנויים." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "שנה סיסמה בסיכון" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "לארגונים חינמיים יכולים להיות עד 2 אוספים. שדרג לתוכנית בתשלום כדי להוסיף עוד אוספים." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index 5e71c274ed9..2ffc414866e 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "External ID is an unencrypted reference used by the Bitwarden Directory Connector and API." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Nest collection under" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn verified successfully! You may close this tab." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Fingerprint" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Remove users" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index 7ab29545495..e5121050ed8 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "Vanjski id je nešifrirana referenca koju koristi Bitwarden Directory Connector i API." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Ugnijezdi zbirku pod" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn uspješno ovjeren! Možeš zatvoriti ovu karticu." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Podsjetnik za lozinku ne može biti isti kao lozinka." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Otisak prsta" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Ukloni korisnike" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Naziv organizacije ne može biti duži od 50 znakova." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "Unesena lozinka nije ispravna." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Dodijeljene licence premašuju dostupne licence." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Promijeni rizičnu lozinku" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Besplatne organizacije mogu imati do 2 zbirke. Nadogradi na plaćeni plan za dodavanje više zbirki." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index dc50065f5d3..266783e836f 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "A külső azonosító a Bitwarden tárkapcsoló és API által nem titkosított referencia." }, + "ssoExternalId": { + "message": "SSO külső azonosító" + }, + "ssoExternalIdDesc": { + "message": "Az SSO külső azonosító egy titkosítatlan hivatkozás a Bitwarden és az jelenleg konfigurált SSO szolgáltató között." + }, "nestCollectionUnder": { "message": "Gyűjtemény beágyazása:" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "A WebAuthn sikeresen ellenőrzésre került.! A fül már bezárható." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Az új jelszó nem lehet azonos a jelenlegi jelszóval." + }, "hintEqualsPassword": { "message": "A jelszavas tipp nem lehet azonos a jelszóval." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Ujjlenyomat" }, + "fingerprintPhrase": { + "message": "Ujjlenyomat kifejezés:" + }, "removeUsers": { "message": "Felhasználók eltávolítása" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "A szervezet neve nem haladhatja meg az 50 karaktert." }, + "rotationCompletedTitle": { + "message": "A kulcsforgatás sikeres volt." + }, + "rotationCompletedDesc": { + "message": "A mesterjelszó és a titkosítási kulcsok frissítésre kerültek. A többi eszköz kijelentkeztetésre került." + }, + "trustUserEmergencyAccess": { + "message": "Bizalom és megerősítés a felhasználónál" + }, + "trustOrganization": { + "message": "Bizalom a szervezetnél" + }, + "trust": { + "message": "Bizalom" + }, + "doNotTrust": { + "message": "Nincs bizalom" + }, + "emergencyAccessTrustWarning": { + "message": "A ióka biztonság érdekében csak akkor erősítsük meg, ha vészhelyzeti hozzáférést biztosítottunk ehhez a felhasználóhoz és az ujjlenyomata megegyezik a fiókban megjelenítettekkel." + }, + "orgTrustWarning": { + "message": "A fiók biztonsága érdekében csak akkor folytassuk, ha tagja vagyunk ennek a szervezetnek, engedélyezve van a fiók helyreállítása és az alább megjelenített ujjlenyomat megegyezik a szervezet ujjlenyomatával." + }, + "trustUser": { + "message": "Bizalom a felhasználónál" + }, "sshKeyWrongPassword": { "message": "A megadott jelszó helytelen." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "A hozzárendelt helyek száma meghaladja a rendelkezésre álló helyek számát." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Ujjlenyomat kifejezés $NUM_USERS$ kapcsolathoz, amelyeknél engedélyeztük a vészhelyzeti hozzáférést.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Ujjlenyomat kifejezés $ORG_NAME$ szervezethez, amelyhez engedélyeztük a fiók helyreállítást.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "A titkosítási kulcsok forgatásához meg kell bízni minden olyan szervezet kulcsában, amely vissza tudja állítani fiókját és minden olyan kapcslatban, amelyhez engedélyeztük a vészhelyzeti hozzáférést. A folytatáshoz győződjünk meg arról, hogy ellenőrizhetjük a következőket:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Megbízhatatlan titkosítási kulcsok" + }, "changeAtRiskPassword": { "message": "Kockázatos jelszó megváltoztatása" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Az ingyenes szervezeteknek legfeljebb 2 gyűjteményük lehet. Térjünk át egy fizetett csomagra további gyűjtemények hozzáadásához." + }, + "businessUnit": { + "message": "Üzleti egység" + }, + "businessUnits": { + "message": "Üzleti egységek" + }, + "newBusinessUnit": { + "message": "Új üzleti egység" } } diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index d62c1dfde53..85d6795e9c1 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "Id eksternal dapat digunakan sebagai referensi atau untuk menautkan sumber daya ini ke sistem eksternal seperti direktori pengguna." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Nest collection under" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn sudah diverifikasi dengan sukses. Anda dapat menutup tab ini." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Petunjuk kata sandi Anda tidak boleh sama dengan kata sandi Anda." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Sidik Jari" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Hapus pengguna" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index 9a0ece95033..893eed1b846 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "L'ID esterno è un riferimento non crittografato usato da Bitwarden Directory Connector e API." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Annida raccolta sotto" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn verificato! Puoi chiudere questa scheda." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Il suggerimento per la password non può essere uguale alla tua password." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Impronta" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Rimuovi utenti" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Il nome dell'organizzazione non può superare i 50 caratteri." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "La parola d'accesso inserita non è corretta." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "I posti assegnati superano i posti disponibili." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Cambia parola d'accesso a rischio" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Le organizzazioni gratuite possono avere fino a 2 raccolte. Aggiorna ad un piano a pagamento per crearne di più." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index 8eb0745bbc1..b9d113b98cd 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "外部 ID は参照用や、ユーザーディレクトリーなどの外部システムへリソースをリンクするために使用できます。" }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "コレクションを入れ子にする" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn が正常に認証されました!このタブを閉じることができます。" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "パスワードのヒントをパスワードと同じにすることはできません。" }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "指紋" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "ユーザーを削除" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "入力されたパスワードが間違っています。" }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "危険なパスワードの変更" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "無料版の組織ではコレクションは 2 つまでです。さらにコレクションを追加するには有料プランにアップグレードしてください。" + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index ed327aff81c..ede2850a6e4 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "External ID is an unencrypted reference used by the Bitwarden Directory Connector and API." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Nest collection under" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn verified successfully! You may close this tab." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Fingerprint" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Remove users" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index 775494830de..293b3b0486f 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "External ID is an unencrypted reference used by the Bitwarden Directory Connector and API." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Nest collection under" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn verified successfully! You may close this tab." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Fingerprint" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Remove users" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index 88edbda5e7b..9074ab87052 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "ಬಾಹ್ಯ ಐಡಿಯನ್ನು ಉಲ್ಲೇಖವಾಗಿ ಬಳಸಬಹುದು ಅಥವಾ ಈ ಸಂಪನ್ಮೂಲವನ್ನು ಬಳಕೆದಾರರ ಡೈರೆಕ್ಟರಿಯಂತಹ ಬಾಹ್ಯ ವ್ಯವಸ್ಥೆಗೆ ಲಿಂಕ್ ಮಾಡಬಹುದು." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Nest collection under" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn ಯಶಸ್ವಿಯಾಗಿ ಪರಿಶೀಲಿಸಲಾಗಿದೆ! ನೀವು ಈ ಟ್ಯಾಬ್ ಅನ್ನು ಮುಚ್ಚಬಹುದು." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "ನಿಮ್ಮ ಪಾಸ್‌ವರ್ಡ್ ಸುಳಿವು ನಿಮ್ಮ ಪಾಸ್‌ವರ್ಡ್‌ನಂತೆಯೇ ಇರಬಾರದು." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "ಫಿಂಗರ್‌ಪ್ರಿಂಟ್" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "ಬಳಕೆದಾರರನ್ನು ತೆಗೆದುಹಾಕಿ" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index 355d1cc08cf..b0868c3e601 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "외부 Id는 참조로 사용되거나 사용자 디렉토리같은 외부 시스템에 리소스를 링크할 수 있습니다." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Nest collection under" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn 인증을 성공적으로 완료했습니다!
이제 이 탭을 닫아도 좋습니다." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "비밀번호 힌트는 비밀번호와 같을 수 없습니다." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "지문" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "사용자 삭제" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index dceb9b2be60..fba3376ac06 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -1124,7 +1124,7 @@ "message": "Izmantota šifrēšanai" }, "loginWithPasskeyEnabled": { - "message": "Pieteikšanās ar piekūves atslēgu ieslēgta" + "message": "Pieteikšanās ar piekļuves atslēgu ieslēgta" }, "passkeySaved": { "message": "$NAME$ saglabāta", @@ -1327,7 +1327,7 @@ "message": "E-pasta adrese" }, "yourVaultIsLockedV2": { - "message": "Glabātava ir slēgta." + "message": "Glabātava ir aizslēgta" }, "yourAccountIsLocked": { "message": "Konts ir slēgts" @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "Ārējo ID var izmanto kā atsauci vai kā saikni starp šo līdzekli un ārēju sistēmu, piemēram, lietotāju direktoriju." }, + "ssoExternalId": { + "message": "SSO ārējais Id" + }, + "ssoExternalIdDesc": { + "message": "SSO ārējais Id ir nešifrēta atsauce starp Bitwarden un konfigurēto SSO nodrošinātāju." + }, "nestCollectionUnder": { "message": "Ievietot krājumu zem" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn tika veiksmīgi apstiprināts.
Šo cilni var aizvērt." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Jaunā parole nevar būt tāda pati kā pašreizējā." + }, "hintEqualsPassword": { "message": "Paroles norāde nedrīkst būt tāda pati kā parole." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Pirkstu nospiedums" }, + "fingerprintPhrase": { + "message": "Atpazīšanas vārdkopa:" + }, "removeUsers": { "message": "Noņemt lietotājus" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Apvienības nosaukums nevar pārsniegt 50 rakstzīmes." }, + "rotationCompletedTitle": { + "message": "Atslēgu nomaiņa sekmīga" + }, + "rotationCompletedDesc": { + "message": "Galvenā parole un šifrēšanas atslēgas tika atjauninātas. Citās ierīcēs ir notikusi atteikšanās." + }, + "trustUserEmergencyAccess": { + "message": "Uzticēties un apstiprināt lietotāju" + }, + "trustOrganization": { + "message": "Uzticēties apvienībai" + }, + "trust": { + "message": "Uzticēties" + }, + "doNotTrust": { + "message": "Neuzticēties" + }, + "emergencyAccessTrustWarning": { + "message": "Lai nodrošinātu sava konta drošību, jāapstiprina tikai tad, ja šim lietotājam ir nodrošināta ārkārtas piekļuve un tā pirkstu nospiedums atbilsta tam, kas ir attēlots tā kontā" + }, + "orgTrustWarning": { + "message": "Lai nodrošinātu sava konta drošību, jāturpina tikai tad, ja esi šīs apvienības dalībnieks, ir iespējota konta atkope un zemāk attēlotais pirkstu nospiedums atbilst apvienības pirkstu nospiedumam." + }, + "trustUser": { + "message": "Uzticēties lietotājam" + }, "sshKeyWrongPassword": { "message": "Ievadītā parole ir nepareiza." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Piešķirtās vietas pārsniedz pieejamās vietas." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Atpazīšanas vārdkopa $NUM_USERS$ kontaktpersonām, kurām ir iespējota ārkārtas piekļuve.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Atpazīšanas vārdkopa apvienībai $ORG_NAME$, kurā ir iespējota konta atkope.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Savu šifrēšanas atslēgu nomaiņai būs nepieciešams uzticēties jebkuras apvienības, kura var atkopt kontu, un kontaktpersonas, kurai ir iespējota ārkārtas piekļuve, atslēgām. Lai turpinātu, jāpārliecināš, ka var apliecināt šo:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Neuzticamas šifrēšanas atslēgas" + }, "changeAtRiskPassword": { "message": "Mainīt riskam pakļautu paroli" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Apvienībās, kuras izmanto Bitwarden bez maksas, var būt līdz 2 krājumiem. Jāpāriet uz maksas plānu, lai pievienotu vairāk krājumu." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index 5876b486bbf..620d1285e65 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "ബാഹ്യ ഐഡി ഒരു റഫറൻസായി ഉപയോഗിക്കാം അല്ലെങ്കിൽ ഈ ഉറവിടം ഒരു ഉപയോക്തൃ ഡയറക്ടറി പോലുള്ള ഒരു ബാഹ്യ സിസ്റ്റത്തിലേക്ക് ലിങ്കുചെയ്യാൻ കഴിയും." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Nest collection under" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn verified successfully! You may close this tab." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Fingerprint" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Remove users" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index 775494830de..293b3b0486f 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "External ID is an unencrypted reference used by the Bitwarden Directory Connector and API." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Nest collection under" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn verified successfully! You may close this tab." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Fingerprint" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Remove users" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index 775494830de..293b3b0486f 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "External ID is an unencrypted reference used by the Bitwarden Directory Connector and API." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Nest collection under" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn verified successfully! You may close this tab." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Fingerprint" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Remove users" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index c68d1bf2961..c31140eb136 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "Den eksterne Id-en kan brukes som referanse eller for å koble denne ressursen til et eksternt system, for eksempel en brukerkatalog." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Plasser samling under" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn bekreftet vellykket! Du kan lukke denne fanen." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Passordhintet ditt kan ikke være det samme som passordet ditt." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Fingeravtrykk" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Fjern brukere" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index f64cfab4ba1..6052556f397 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "External ID is an unencrypted reference used by the Bitwarden Directory Connector and API." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Nest collection under" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn verified successfully! You may close this tab." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Fingerprint" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Remove users" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index 93eca761180..22eb954f5c4 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "Je kunt het externe ID gebruiken als referentie of om een koppeling te leggen tussen deze bron en een extern systeem zoals een directory met gebruikers." }, + "ssoExternalId": { + "message": "SSO extern ID" + }, + "ssoExternalIdDesc": { + "message": "SSO Extern ID is een niet-versleutelde referentie tussen Bitwarden en je geconfigureerde SSO-provider." + }, "nestCollectionUnder": { "message": "Plaats collectie onder" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn met succes geverifieerd! Je kunt dit tabblad sluiten." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Je nieuwe wachtwoord moet anders zijn dan je huidige wachtwoord." + }, "hintEqualsPassword": { "message": "Je wachtwoordhint moet anders zijn dan je wachtwoord." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Vingerafdruk" }, + "fingerprintPhrase": { + "message": "Vingerafdrukzin:" + }, "removeUsers": { "message": "Gebruikers verwijderen" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organisatienaam mag niet langer zijn dan 50 tekens." }, + "rotationCompletedTitle": { + "message": "Sleutelrotatie geslaagd" + }, + "rotationCompletedDesc": { + "message": "Je hoofdwachtwoord en encryptiesleutels zijn bijgewerkt. Je andere apparaten zijn uitgelogd." + }, + "trustUserEmergencyAccess": { + "message": "Gebruiker vertrouwen en bevestigen" + }, + "trustOrganization": { + "message": "Organisatie vertrouwen" + }, + "trust": { + "message": "Vertrouwen" + }, + "doNotTrust": { + "message": "Niet vertrouwen" + }, + "emergencyAccessTrustWarning": { + "message": "Bevestig, voor de veiligheid van je account, alleen als je noodtoegang hebt verleend aan deze gebruiker en de vingerafdruk voldoet aan wat er in hun account wordt weergegeven" + }, + "orgTrustWarning": { + "message": "Ga, voor de veiligheid van je account, alleen verder als je lid bent van deze organisatie, accountherstel hebt ingeschakeld en de hieronder weergegeven vingerafdruk overeenkomt met de vingerafdruk van de organisatie." + }, + "trustUser": { + "message": "Gebruiker vertrouwen" + }, "sshKeyWrongPassword": { "message": "Het door jou ingevoerde wachtwoord is onjuist." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Meer toegewezen dan beschikbare plaatsen." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Vingerafdrukzin voor $NUM_USERS$ contacten waarvoor je noodtoegang hebt ingeschakeld.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Vingerafdrukzin voor de organisatie $ORG_NAME$ waarvoor je accountherstel hebt ingeschakeld.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Het roteren van je encryptiesleutels vereist dat je sleutels vertrouwt van alle organisaties die je account kunnen herstellen en alle contacten waarvoor je noodtoegang hebt ingeschakeld. Zorg ervoor dat je het volgende kunt verifiëren:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Niet-vertrouwde encryptiesleutels" + }, "changeAtRiskPassword": { "message": "Risicovol wachtwoord wijzigen" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Gratis organisaties kunnen maximaal twee collecties hebben. Upgrade naar een betaald abonnement voor het toevoegen van meer collecties." + }, + "businessUnit": { + "message": "Bedrijfseenheid" + }, + "businessUnits": { + "message": "Bedrijfseenheden" + }, + "newBusinessUnit": { + "message": "Nieuwe bedrijfseenheid" } } diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index c50af10221a..41d012ac3e9 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "External ID is an unencrypted reference used by the Bitwarden Directory Connector and API." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Nest collection under" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn verified successfully! You may close this tab." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Fingerprint" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Remove users" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index 775494830de..293b3b0486f 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "External ID is an unencrypted reference used by the Bitwarden Directory Connector and API." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Nest collection under" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn verified successfully! You may close this tab." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Fingerprint" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Remove users" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index b4b4c90d44a..7fa157e523c 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "Zewnętrzny identyfikator może zostać użyty jako odniesienie lub połączenie tego zasobu z zewnętrznym systemem, takim jak katalog użytkowników." }, + "ssoExternalId": { + "message": "Zewnętrzny identyfikator SSO" + }, + "ssoExternalIdDesc": { + "message": "Zewnętrzny identyfikator SSO jest niezaszyfrowanym odniesieniem między Bitwarden a Twoim skonfigurowanym dostawcą SSO." + }, "nestCollectionUnder": { "message": "Zagnieźdź kolekcję pod" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "Uwierzytelnianie WebAuthn zostało zweryfikowane! Możesz zamknąć tę kartę." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Twoje nowe hasło nie może być takie samo jak Twoje aktualne hasło." + }, "hintEqualsPassword": { "message": "Podpowiedź do hasła nie może być taka sama jak hasło." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Unikalny identyfikator konta" }, + "fingerprintPhrase": { + "message": "Fraza odcisku palca:" + }, "removeUsers": { "message": "Usuń użytkowników" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Nazwa organizacji nie może przekraczać 50 znaków." }, + "rotationCompletedTitle": { + "message": "Rotacja klucza powiodło się" + }, + "rotationCompletedDesc": { + "message": "Twoje hasło główne i klucze szyfrowania zostały zaktualizowane. Twoje inne urządzenia zostały wylogowane." + }, + "trustUserEmergencyAccess": { + "message": "Zaufaj i potwierdź użytkownika" + }, + "trustOrganization": { + "message": "Zaufaj organizacji" + }, + "trust": { + "message": "Zaufaj" + }, + "doNotTrust": { + "message": "Nie ufaj" + }, + "emergencyAccessTrustWarning": { + "message": "Dla bezpieczeństwa Twojego konta potwierdź tylko, jeśli przyznano temu użytkownikowi dostęp awaryjny i jego odcisk palca pasuje do tego, co widnieje na jego koncie" + }, + "orgTrustWarning": { + "message": "Dla zapewnienia bezpieczeństwa konta kontynuuj tylko wtedy, gdy jesteś członkiem tej organizacji, włączono odzyskiwanie konta, a odcisk palca wyświetlany poniżej pasuje do odcisku palca organizacji." + }, + "trustUser": { + "message": "Zaufaj użytkownikowi" + }, "sshKeyWrongPassword": { "message": "Wprowadzone hasło jest nieprawidłowe." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Przydzielone miejsca przekraczają dostępne miejsca." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fraza odcisku palca dla $NUM_USERS$ kontaktów, dla których włączono dostęp awaryjny.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fraza odcisku palca dla organizacji $ORG_NAME$ dla której włączyłeś odzyskiwanie konta.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotacja kluczy szyfrowania będzie wymagała zaufanych kluczy każdej organizacji, która może odzyskać Twoje konto oraz wszystkich kontaktów, które mają udostępniony dostęp awaryjny. Aby kontynuować, upewnij się, że możesz zweryfikować następujące elementy:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Niezaufane klucze szyfrowania" + }, "changeAtRiskPassword": { "message": "Zmień hasło zagrożone" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Darmowe organizacje mogą posiadać maksymalnie 2 kolekcje. Aby dodać więcej kolekcji, przejdź na plan płatny." + }, + "businessUnit": { + "message": "Jednostka Biznesowa" + }, + "businessUnits": { + "message": "Jednostki Biznesowe" + }, + "newBusinessUnit": { + "message": "Nowa jednostka biznesowa" } } diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index 1b89aa405f0..4cb7f19a1fc 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "O ID externo pode ser usado como referência ou vincular esse recurso a um sistema externo, como um diretório de usuários." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Aninhar coleção em" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn verificado com sucesso!
Você pode fechar esta guia." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Sua nova senha não pode ser a mesma que a sua atual." + }, "hintEqualsPassword": { "message": "Sua dica de senha senha não pode ser a mesma que a sua senha." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Impressão digital" }, + "fingerprintPhrase": { + "message": "Frase biométrica:" + }, "removeUsers": { "message": "Remover Usuários" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "O nome da organização não pode exceder 50 caracteres." }, + "rotationCompletedTitle": { + "message": "Rotação de chave bem-sucedida" + }, + "rotationCompletedDesc": { + "message": "Sua senha mestra e chave de criptografia foram atualizadas. Seus outros dispositivos foram desconectados." + }, + "trustUserEmergencyAccess": { + "message": "Confiar e confirmar usuário" + }, + "trustOrganization": { + "message": "Organização de confiança" + }, + "trust": { + "message": "Confiança" + }, + "doNotTrust": { + "message": "Não confiar" + }, + "emergencyAccessTrustWarning": { + "message": "Para a segurança de sua conta, só confirme se você concedeu acesso de emergência a este usuário e sua impressão digital corresponde ao que é exibido na conta dele" + }, + "orgTrustWarning": { + "message": "Para a segurança da sua conta, prossiga somente se você for um membro desta organização, tem a recuperação de conta ativada, e a impressão digital exibida abaixo corresponde à impressão digital da organização." + }, + "trustUser": { + "message": "Confiar no usuário" + }, "sshKeyWrongPassword": { "message": "A senha está incorreta." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Os assentos atribuídos excedem os disponíveis." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Frase de impressão digital para contatos $NUM_USERS$ para os quais você ativou o acesso de emergência.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Frase de impressão digital para a organização $ORG_NAME$ para a qual você ativou a recuperação de conta.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Girar suas chaves de criptografia exigirá que você confie nas chaves de qualquer organização que possa recuperar sua conta, e quaisquer contatos que você ativou o acesso de emergência. Para continuar, certifique-se de que pode verificar o seguinte:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Chaves criptográficas não confiáveis" + }, "changeAtRiskPassword": { "message": "Alterar senhas vulneráveis" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Organizações gratuitas podem ter até duas coleções. Faça o upgrade para um plano pago para adicionar mais coleções." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index d45484af588..3840d5fd0fd 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -3318,10 +3318,16 @@ "message": "Atenção! Este utilizador necessita do Key Connector para gerir a sua encriptação. Ao remover este utilizador da sua organização desativará permanentemente a sua conta. Esta ação não pode ser anulada. Deseja prosseguir?" }, "externalId": { - "message": "ID externa" + "message": "ID externo" }, "externalIdDesc": { - "message": "A ID externa é uma referência não encriptada utilizada pelo Bitwarden Directory Connector e pela API." + "message": "O ID externo é uma referência desencriptada utilizada pelo Bitwarden Directory Connector e pela API." + }, + "ssoExternalId": { + "message": "ID externo SSO" + }, + "ssoExternalIdDesc": { + "message": "O ID externo SSO é uma referência desencriptada entre o Bitwarden e o seu fornecedor de SSO configurado." }, "nestCollectionUnder": { "message": "Aninhar coleção em" @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn verificado com sucesso! Pode fechar este separador." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "A sua nova palavra-passe não pode ser igual à sua palavra-passe atual." + }, "hintEqualsPassword": { "message": "A dica da sua palavra-passe não pode ser igual à sua palavra-passe." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Impressão digital" }, + "fingerprintPhrase": { + "message": "Frase de impressão digital:" + }, "removeUsers": { "message": "Remover utilizadores" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "O nome da organização não pode exceder 50 carateres." }, + "rotationCompletedTitle": { + "message": "Rotação de chaves bem-sucedida" + }, + "rotationCompletedDesc": { + "message": "A sua palavra-passe mestra e as chaves de encriptação foram atualizadas. Foi terminada a sessão nos seus outros dispositivos." + }, + "trustUserEmergencyAccess": { + "message": "Confiar e confirmar o utilizador" + }, + "trustOrganization": { + "message": "Confiar na organização" + }, + "trust": { + "message": "Confiar" + }, + "doNotTrust": { + "message": "Não confiar" + }, + "emergencyAccessTrustWarning": { + "message": "Para segurança da sua conta, confirme apenas se tiver concedido acesso de emergência a este utilizador e se a sua impressão digital corresponder à que é apresentada na sua conta" + }, + "orgTrustWarning": { + "message": "Para segurança da sua conta, prossiga apenas se for membro desta organização, tiver a recuperação de conta ativada e a impressão digital apresentada abaixo corresponder à impressão digital da organização." + }, + "trustUser": { + "message": "Confiar no utilizador" + }, "sshKeyWrongPassword": { "message": "A palavra-passe que introduziu está incorreta." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Os lugares atribuídos excedem os lugares disponíveis." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Frase de impressão digital dos $NUM_USERS$ contactos para os quais ativou o acesso de emergência.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Frase de impressão digital para a organização $ORG_NAME$ para a qual ativou a recuperação de conta.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "A rotação das suas chaves de encriptação exigirá que confie nas chaves de quaisquer organizações que possam recuperar a sua conta e em quaisquer contactos para os quais tenha ativado o acesso de emergência. Para continuar, certifique-se de que pode verificar o seguinte:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Chaves de encriptação não fiáveis" + }, "changeAtRiskPassword": { "message": "Alterar palavra-passe em risco" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "As organizações gratuitas podem ter até 2 coleções. Atualize para um plano pago para adicionar mais coleções." + }, + "businessUnit": { + "message": "Unidade de negócio" + }, + "businessUnits": { + "message": "Unidades de negócio" + }, + "newBusinessUnit": { + "message": "Nova unidade de negócio" } } diff --git a/apps/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index 9531fdaa59a..e513deb7f5e 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "Id-ul extern poate fi utilizat ca referință sau pentru a lega această resursă de un sistem extern, cum ar fi un folder utilizator." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Nest collection under" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn a fost verificat cu succes! Puteți închide această filă." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Indiciul dvs. de parolă nu poate fi aceeași cu parola." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Amprentă" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Eliminare utilizatori" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index cc2ffc2e524..f1bb004ce1d 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "Внешний ID - это незашифрованная ссылка, используемая Bitwarden Directory Connector и API." }, + "ssoExternalId": { + "message": "Внешний ID SSO" + }, + "ssoExternalIdDesc": { + "message": "Внешний ID SSO - это незашифрованная ссылка между Bitwarden и вашим настроенным провайдером SSO." + }, "nestCollectionUnder": { "message": "Разместить коллекцию под:" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn успешно прошел верификацию!
Вы можете закрыть эту вкладку." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Ваш новый пароль не может быть таким же, как текущий." + }, "hintEqualsPassword": { "message": "Подсказка для пароля не может совпадать с паролем." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Отпечаток" }, + "fingerprintPhrase": { + "message": "Фраза отпечатка:" + }, "removeUsers": { "message": "Удалить пользователей" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Название организации не может превышать 50 символов." }, + "rotationCompletedTitle": { + "message": "Изменение ключа успешно" + }, + "rotationCompletedDesc": { + "message": "Ваш мастер-пароль и ключ шифрования были обновлены. Другие ваши устройства были выведены из системы." + }, + "trustUserEmergencyAccess": { + "message": "Доверенный и подтвержденный пользователь" + }, + "trustOrganization": { + "message": "Доверенная организация" + }, + "trust": { + "message": "Доверять" + }, + "doNotTrust": { + "message": "Не доверять" + }, + "emergencyAccessTrustWarning": { + "message": "Для обеспечения безопасности вашей учетной записи подтвердите, только если вы предоставили этому пользователю экстренный доступ и его отпечаток пальца соответствует тому, что отображается в его учетной записи" + }, + "orgTrustWarning": { + "message": "Для обеспечения безопасности вашей учетной записи действуйте только в том случае, если вы являетесь членом этой организации, у вас включено восстановление учетной записи, и отпечаток пальца, отображаемый ниже, соответствует отпечатку пальца организации." + }, + "trustUser": { + "message": "Доверенный пользователь" + }, "sshKeyWrongPassword": { "message": "Введенный пароль неверен." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Количество назначенных мест превышает количество доступных." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Фраза отпечатка пальца для контактов $NUM_USERS$, для которых вы включили экстренный доступ.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Фраза отпечатка пальца для организации $ORG_NAME$, для которой вы включили восстановление учетной записи.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Изменение ключей шифрования потребует от вас доверия к ключам любых организаций, которые могут восстановить вашу учетную запись, а также любым контактам, для которых вы включили экстренный доступ. Для продолжения убедитесь, что вы можете проверить следующее:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Не доверенные ключи шифрования" + }, "changeAtRiskPassword": { "message": "Изменить пароль, подверженный риску" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "В бесплатных организациях может быть до 2 коллекций. Перейдите на платный план, чтобы добавить больше коллекций." + }, + "businessUnit": { + "message": "Бизнес-единица" + }, + "businessUnits": { + "message": "Бизнес-единицы" + }, + "newBusinessUnit": { + "message": "Новая бизнес-единица" } } diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index 8583aee42de..2acc04e756d 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "External ID is an unencrypted reference used by the Bitwarden Directory Connector and API." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Nest collection under" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn verified successfully! You may close this tab." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Fingerprint" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Remove users" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index 6a1e2ba715b..0c3c4eaba26 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "Externé Id sa môže použiť na previazanie tohto zdroja s externým systémom - napríklad s užívateľským adresárom." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Zaradiť zbierku pod" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn bol úspešne overený! Túto kartu môžete zavrieť." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Nápoveda pre heslo nemôže byť rovnaká ako heslo." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Odtlačok prsta" }, + "fingerprintPhrase": { + "message": "Fráza odtlačku prsta:" + }, "removeUsers": { "message": "Odstrániť používateľov" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Meno organizácie nemôže mať viac ako 50 znakov." }, + "rotationCompletedTitle": { + "message": "Obnova kľúča úspešná" + }, + "rotationCompletedDesc": { + "message": "Vaše hlavné heslo a šifrovacie kľúče boli aktualizované. Vaše ostatné zariadenia boli odhlásené." + }, + "trustUserEmergencyAccess": { + "message": "Dôverovať a povoliť používateľa" + }, + "trustOrganization": { + "message": "Dôverovať organizácii" + }, + "trust": { + "message": "Dôverovať" + }, + "doNotTrust": { + "message": "Nedôverovať" + }, + "emergencyAccessTrustWarning": { + "message": "Pre bezpečnosť vášho konta stačí overiť, ze ste tomuto používateľovi udelili núdzový pristúp a že odtlačok sa zhoduje s odtlačkom zobrazenom v používateľovom konte" + }, + "orgTrustWarning": { + "message": "Pre bezpečnosť vášho konta pokračujte iba ak ste členom organizácie, mate povolenú obnovu konta a odtlačok sa zhoduje s odtlačkom organizácie." + }, + "trustUser": { + "message": "Dôverovať používateľovi" + }, "sshKeyWrongPassword": { "message": "Zadané heslo je nesprávne." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Počet pridelených sedení presahuje počet dostupných sedení." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fráza odtlačku kontaktu $NUM_USERS$ pre ktorý(é) ste povolili núdzový prístup.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fráza odtlačku organizácie $ORG_NAME$ pre ktorú(é) ste povolili obnovu konta.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Obnovenie šifrovacieho kľúča vyžaduje aby ste dôverovali organizáciám ktoré môžu obnoviť vaše konto a kontaktom pre ktoré ste povolili núdzový pristúp. Ak chcete pokračovať, uistite sa že ste overili nasledujúce:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Nedôveryhodné šifrovacie kľúče" + }, "changeAtRiskPassword": { "message": "Zmeniť ohrozené heslo" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Bezplatné organizácie môžu mat maximálne dve zbierky. Ak chcete pridať viac zbierok povýšte na platené predplatné." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index 0357dca3932..fd990234b86 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "External ID is an unencrypted reference used by the Bitwarden Directory Connector and API." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Nest collection under" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn verified successfully! You may close this tab." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Fingerprint" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Remove users" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/sr/messages.json b/apps/web/src/locales/sr/messages.json index 7ddbbb746de..1a414d913a7 100644 --- a/apps/web/src/locales/sr/messages.json +++ b/apps/web/src/locales/sr/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "Спољни ид се може користити као референца или за повезивање овог ресурса са спољним системом као што је корисничка фасцикла." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Постави колекцију испод" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "Успешна провера WebAuthn-а!
Можете да затворите овај језичак." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Ваша помоћ за лозинку не може да буде иста као лозинка." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Отисак прста" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Уклони кориснике" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Име организације не може прећи 50 знакова." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "Лозинка коју сте унели није тачна." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Променити ризичну лозинку" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Бесплатне организације могу имати до 2 колекције. Надоградите на плаћени план за додавање више колекција." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index a27221c469e..93f2c89a16d 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "External ID is an unencrypted reference used by the Bitwarden Directory Connector and API." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Nest collection under" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn verified successfully! You may close this tab." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Fingerprint" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Remove users" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index aced26c606f..5005b6180c7 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "Det externa id:t kan användas som referens eller för att länka denna resurs till ett externt system såsom en användarkatalog." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Kapsla samling under" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn verified successfully! You may close this tab." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Din lösenordsledtråd får inte vara samma som ditt lösenord." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Fingeravtryck" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Ta bort användare" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index 775494830de..293b3b0486f 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "External ID is an unencrypted reference used by the Bitwarden Directory Connector and API." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Nest collection under" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn verified successfully! You may close this tab." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Fingerprint" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Remove users" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index 73d7289939b..7ea003470cb 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "External ID is an unencrypted reference used by the Bitwarden Directory Connector and API." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Nest collection under" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn verified successfully! You may close this tab." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Fingerprint" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Remove users" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index a9b1aaa5edf..da4b35b403e 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "Harici kimlik, referans olarak veya bu kaynağı kullanıcı dizini gibi harici bir sisteme bağlamak için kullanılabilir." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Koleksiyonu bunun altına yerleştir" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn başarıyla doğrulandı. Bu sekmeyi kapatabilirsiniz." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Parola ipucunuz parolanızla aynı olamaz." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Parmak izi" }, + "fingerprintPhrase": { + "message": "Parmak izi ifadesi:" + }, "removeUsers": { "message": "Kullanıcıları kaldır" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "Girdiğiniz parola yanlış." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Ücretsiz kuruluşların en fazla 2 koleksiyonu olabilir. Daha fazla koleksiyon eklemek için ücretli bir plana geçin." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index acd79b5dc26..834bb36adfa 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "Зовнішній ID – це незашифроване посилання, призначене для використання Bitwarden Directory Connector і API." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Розмістити збірку під" }, @@ -5685,8 +5691,11 @@ "webAuthnSuccess": { "message": "WebAuthn успішно підтверджено! Ви можете закрити цю вкладку." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { - "message": "Підказка для пароля не може бути такою самою, як ваш пароль." + "message": "Підказка повинна відрізнятися від пароля." }, "enrollAccountRecovery": { "message": "Розгорнути на відновлення облікового запису" @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Цифровий відбиток" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Вилучити користувачів" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Назва організації не може перевищувати 50 символів." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "Ви ввели неправильний пароль." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Призначені місця перевищують доступні місця." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Змінити ризикований пароль" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Безплатні організації можуть мати до 2 збірок. Передплатіть тарифний план, щоб додати більше збірок." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index 1acf336af1d..0f2e31d834a 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "External ID is an unencrypted reference used by the Bitwarden Directory Connector and API." }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "Nest collection under" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn verified successfully! You may close this tab." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "Fingerprint" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "Remove users" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index f8bb62b0b00..5140946175f 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -1638,10 +1638,10 @@ "message": "确认文件密码" }, "accountRestrictedOptionDescription": { - "message": "使用衍生自您账户的用户名和主密码的加密密钥,以加密此导出并限制只能导入到当前的 Bitwarden 账户。" + "message": "使用衍生自您账户用户名和主密码的加密密钥以加密此导出,并限制只能导入到当前 Bitwarden 账户。" }, "passwordProtectedOptionDescription": { - "message": "设置一个密码用来加密导出的数据,并使用此密码解密以导入到任意 Bitwarden 账户。" + "message": "设置一个文件密码以加密此导出,并使用此密码解密以导入到任意 Bitwarden 账户。" }, "exportTypeHeading": { "message": "导出类型" @@ -3321,7 +3321,13 @@ "message": "外部 ID" }, "externalIdDesc": { - "message": "外部 ID 是一个 Bitwarden Directory Connector 和 API 使用的未经加密的参考。" + "message": "外部 ID 是 Bitwarden Directory Connector 和 API 使用的未经加密的参考。" + }, + "ssoExternalId": { + "message": "SSO 外部 ID" + }, + "ssoExternalIdDesc": { + "message": "SSO 外部 ID 是 Bitwarden 与您配置的 SSO 提供程序之间的未经加密的参考。" }, "nestCollectionUnder": { "message": "嵌套于集合下" @@ -4573,7 +4579,7 @@ "message": "随时。" }, "byContinuingYouAgreeToThe": { - "message": "若继续,代表您同意" + "message": "若继续,表示您同意" }, "and": { "message": "和" @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn 验证成功!您可以关闭此标签页。" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "您的新密码不能与当前密码相同。" + }, "hintEqualsPassword": { "message": "密码提示不能与密码相同。" }, @@ -5788,7 +5797,7 @@ "message": "自动注册" }, "resetPasswordPolicyAutoEnrollCheckbox": { - "message": "为新用户启用自动注册" + "message": "为新成员启用自动注册" }, "resetPasswordAutoEnrollInviteWarning": { "message": "此组织有一个企业策略,将自动为您注册密码重置。注册后将允许组织管理员更改您的主密码。" @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "指纹" }, + "fingerprintPhrase": { + "message": "指纹短语:" + }, "removeUsers": { "message": "移除用户" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "组织名称不能超过 50 个字符。" }, + "rotationCompletedTitle": { + "message": "密钥轮换成功" + }, + "rotationCompletedDesc": { + "message": "您的主密码和加密密钥已更新。您的其他设备已被注销。" + }, + "trustUserEmergencyAccess": { + "message": "信任并确认用户" + }, + "trustOrganization": { + "message": "信任组织" + }, + "trust": { + "message": "信任" + }, + "doNotTrust": { + "message": "不信任" + }, + "emergencyAccessTrustWarning": { + "message": "为了您的账户安全,确认前请先确认:您已授予该用户紧急访问权限,以及其指纹与其账户中显示的指纹相匹配" + }, + "orgTrustWarning": { + "message": "为了您的账户安全,继续前请先确认:您是启用了账户恢复功能的该组织的成员,以及下方显示的指纹与此组织的指纹相匹配。" + }, + "trustUser": { + "message": "信任用户" + }, "sshKeyWrongPassword": { "message": "您输入的密码不正确。" }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "分配的席位超过可用席位。" }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "启用了紧急访问权限的 $NUM_USERS$ 位联系人的指纹短语。", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "启用了账户恢复功能的组织 $ORG_NAME$ 的指纹短语。", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "轮换加密密钥将需要您信任能够恢复您账户的所有组织的密钥,以及您已为其启用紧急访问的所有联系人的密钥。要继续操作,请确保您能够验证以下信息:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "未信任的加密密钥" + }, "changeAtRiskPassword": { "message": "更改有风险的密码" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "免费组织最多拥有 2 个集合。要添加更多集合,请升级到付费计划。" + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index d72b53d3aa3..23b932cd52a 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -3323,6 +3323,12 @@ "externalIdDesc": { "message": "外部 ID 可用於參考,或將此資源與使用者目錄等外部系統連結起來。" }, + "ssoExternalId": { + "message": "SSO External ID" + }, + "ssoExternalIdDesc": { + "message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider." + }, "nestCollectionUnder": { "message": "將集合嵌套在" }, @@ -5685,6 +5691,9 @@ "webAuthnSuccess": { "message": "WebAuthn 驗證成功!您可以關閉此分頁。" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "密碼提示不能與您的密碼相同。" }, @@ -5880,6 +5889,9 @@ "fingerprint": { "message": "指紋" }, + "fingerprintPhrase": { + "message": "Fingerprint phrase:" + }, "removeUsers": { "message": "移除使用者" }, @@ -10330,6 +10342,33 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "rotationCompletedTitle": { + "message": "Key rotation successful" + }, + "rotationCompletedDesc": { + "message": "Your master password and encryption keys have been updated. Your other devices have been logged out." + }, + "trustUserEmergencyAccess": { + "message": "Trust and confirm user" + }, + "trustOrganization": { + "message": "Trust organization" + }, + "trust": { + "message": "Trust" + }, + "doNotTrust": { + "message": "Do not trust" + }, + "emergencyAccessTrustWarning": { + "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" + }, + "orgTrustWarning": { + "message": "For the security of your account, only proceed if you are a member of this organization, have account recovery enabled, and the fingerprint displayed below matches the organization's fingerprint." + }, + "trustUser": { + "message": "Trust user" + }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." }, @@ -10499,6 +10538,30 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "userkeyRotationDisclaimerEmergencyAccessText": { + "message": "Fingerprint phrase for $NUM_USERS$ contacts for which you have enabled emergency access.", + "placeholders": { + "num_users": { + "content": "$1", + "example": "5" + } + } + }, + "userkeyRotationDisclaimerAccountRecoveryOrgsText": { + "message": "Fingerprint phrase for the organization $ORG_NAME$ for which you have enabled account recovery.", + "placeholders": { + "org_name": { + "content": "$1", + "example": "My org" + } + } + }, + "userkeyRotationDisclaimerDescription": { + "message": "Rotating your encryption keys will require you to trust keys of any organizations that can recover your account, and any contacts that you have enabled emergency access for. To continue, make sure you can verify the following:" + }, + "userkeyRotationDisclaimerTitle": { + "message": "Untrusted encryption keys" + }, "changeAtRiskPassword": { "message": "Change at-risk password" }, @@ -10528,5 +10591,14 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "businessUnit": { + "message": "Business Unit" + }, + "businessUnits": { + "message": "Business Units" + }, + "newBusinessUnit": { + "message": "New business unit" } } From b5434adb420c0a2ed923e989c8a2e50b6855471f Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Tue, 15 Apr 2025 13:32:36 +0200 Subject: [PATCH 16/47] Autosync the updated translations (#14282) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/ru/messages.json | 2 +- apps/desktop/src/locales/zh_CN/messages.json | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index 193a6352254..870efba3c9e 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -157,7 +157,7 @@ "message": "Код безопасности" }, "identityName": { - "message": "Имя" + "message": "Название личности" }, "company": { "message": "Компания" diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 3dd411f6593..38892deb282 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -1672,7 +1672,7 @@ "message": "无障碍 Cookie 已保存!" }, "noAccessibilityCookieSaved": { - "message": "无障碍 Cookie 未保存!" + "message": "未保存任何无障碍 Cookie!" }, "warning": { "message": "警告", @@ -2916,7 +2916,7 @@ "message": "然后继续创建您的账户。" }, "noEmail": { - "message": "没收到电子邮件吗?" + "message": "没有收到电子邮件吗?" }, "goBack": { "message": "返回" @@ -3434,7 +3434,7 @@ "message": "文本 Send" }, "ssoError": { - "message": "找不到用于 SSO 登录的可用端口。" + "message": "未找到用于 SSO 登录的空闲端口。" }, "biometricsStatusHelptextUnlockNeeded": { "message": "生物识别解锁不可用,因为需要先使用 PIN 或密码解锁。" From c7804c103215033fdba4c4732d673a76f9294f00 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Tue, 15 Apr 2025 13:34:13 +0200 Subject: [PATCH 17/47] Autosync the updated translations (#14283) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/src/locales/az/messages.json | 6 +++--- apps/web/src/locales/bg/messages.json | 6 +++--- apps/web/src/locales/lv/messages.json | 8 ++++---- apps/web/src/locales/nl/messages.json | 2 +- apps/web/src/locales/pt_BR/messages.json | 10 +++++----- apps/web/src/locales/ru/messages.json | 22 +++++++++++----------- apps/web/src/locales/uk/messages.json | 4 ++-- apps/web/src/locales/zh_CN/messages.json | 18 +++++++++--------- 8 files changed, 38 insertions(+), 38 deletions(-) diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index 0117fa37ae4..78b0b1ec825 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -10593,12 +10593,12 @@ "message": "Ödənişsiz təşkilatların ən çox 2 kolleksiyası ola bilər. Daha çox kolleksiya əlavə etmək üçün ödənişli bir plana yüksəldin." }, "businessUnit": { - "message": "Business Unit" + "message": "Biznes vahidi" }, "businessUnits": { - "message": "Business Units" + "message": "Biznes vahidləri" }, "newBusinessUnit": { - "message": "New business unit" + "message": "Yeni biznes vahidi" } } diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index de23b298710..13bbdc14d4a 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -10593,12 +10593,12 @@ "message": "Безплатните организации могат да имат не повече от 2 колекции. Надградете до платен план, ако искате да имате повече колекции." }, "businessUnit": { - "message": "Business Unit" + "message": "Бизнес единица" }, "businessUnits": { - "message": "Business Units" + "message": "Бизнес единици" }, "newBusinessUnit": { - "message": "New business unit" + "message": "Нова бизнес единица" } } diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index fba3376ac06..3faf2a549de 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -7028,7 +7028,7 @@ } }, "forwarderNoDomain": { - "message": "Nederīgs $SERVICENAME$ domēna vārds.", + "message": "Nederīgs $SERVICENAME$ domēns.", "description": "Displayed when the domain is empty or domain authorization failed at the forwarding service.", "placeholders": { "servicename": { @@ -10593,12 +10593,12 @@ "message": "Apvienībās, kuras izmanto Bitwarden bez maksas, var būt līdz 2 krājumiem. Jāpāriet uz maksas plānu, lai pievienotu vairāk krājumu." }, "businessUnit": { - "message": "Business Unit" + "message": "Uzņēmējdarbības vienība" }, "businessUnits": { - "message": "Business Units" + "message": "Uzņēmējdarbības vienības" }, "newBusinessUnit": { - "message": "New business unit" + "message": "Jauna uzņēmējdarbības vienība" } } diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index 22eb954f5c4..b6811d9289f 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -6,7 +6,7 @@ "message": "Belangrijke applicaties" }, "noCriticalAppsAtRisk": { - "message": "Geen belangrijke applicaties lopen risico" + "message": "Geen kritische applicaties in gevaar" }, "accessIntelligence": { "message": "Toegangsintelligentie" diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index 4cb7f19a1fc..70cbc35fe6c 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -244,7 +244,7 @@ "message": "Chave do autenticador" }, "autofillOptions": { - "message": "Opções de autopreenchimento" + "message": "Opções de preenchimento automático" }, "websiteUri": { "message": "Site (URI)" @@ -297,7 +297,7 @@ } }, "autoFillOnPageLoad": { - "message": "Preenchimento automático ao carregar a página?" + "message": "Preencher automaticamente ao carregar a página?" }, "number": { "message": "Número" @@ -6174,13 +6174,13 @@ "message": "Ativar preenchimento automático" }, "activateAutofillPolicyDesc": { - "message": "Ative o autopreenchimento na configuração de carregamento de página na extensão do navegador para todos os membros existentes e novos." + "message": "Ative a configuração de preenchimento automático ao carregamento de página na extensão do navegador para todos os membros existentes e novos." }, "experimentalFeature": { "message": "Sites comprometidos ou não confiáveis podem tomar vantagem do autopreenchimento ao carregar a página." }, "learnMoreAboutAutofill": { - "message": "Saiba mais sobre preenchimento automático" + "message": "Saiba mais sobre o preenchimento automático" }, "selectType": { "message": "Selecionar Tipo de SSO" @@ -10049,7 +10049,7 @@ "message": "Use caixas de seleção se gostaria de preencher automaticamente a caixa de seleção de um formulário, como um e-mail de lembrança" }, "linkedHelpText": { - "message": "Use um campo vinculado quando estiver enfrentando problemas com o auto-preenchimento para um site específico." + "message": "Use um campo vinculado quando estiver enfrentando problemas com o preenchimento automático em um site específico." }, "linkedLabelHelpText": { "message": "Digite o Id html do campo, nome, nome aria-label, ou marcador de posição" diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index f1bb004ce1d..e84af0d0b54 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -33,7 +33,7 @@ "message": "Уведомленные участники" }, "revokeMembers": { - "message": "Отзыв пользователей" + "message": "Отозвать пользователей" }, "restoreMembers": { "message": "Восстановление пользователей" @@ -315,7 +315,7 @@ "message": "Код безопасности / CVV" }, "identityName": { - "message": "Имя" + "message": "Название личности" }, "company": { "message": "Компания" @@ -10343,13 +10343,13 @@ "message": "Название организации не может превышать 50 символов." }, "rotationCompletedTitle": { - "message": "Изменение ключа успешно" + "message": "Успешная ротация ключа" }, "rotationCompletedDesc": { - "message": "Ваш мастер-пароль и ключ шифрования были обновлены. Другие ваши устройства были выведены из системы." + "message": "Ваш мастер-пароль и ключи шифрования обновлены. Остальные устройства были разлогинены." }, "trustUserEmergencyAccess": { - "message": "Доверенный и подтвержденный пользователь" + "message": "Удостоверить и подтвердить пользователя" }, "trustOrganization": { "message": "Доверенная организация" @@ -10361,10 +10361,10 @@ "message": "Не доверять" }, "emergencyAccessTrustWarning": { - "message": "Для обеспечения безопасности вашей учетной записи подтвердите, только если вы предоставили этому пользователю экстренный доступ и его отпечаток пальца соответствует тому, что отображается в его учетной записи" + "message": "В целях обеспечения безопасности вашего аккаунта подтверждайте только в том случае, если вы предоставили этому пользователю экстренный доступ и его отпечаток совпадает с отображаемым в его аккаунте" }, "orgTrustWarning": { - "message": "Для обеспечения безопасности вашей учетной записи действуйте только в том случае, если вы являетесь членом этой организации, у вас включено восстановление учетной записи, и отпечаток пальца, отображаемый ниже, соответствует отпечатку пальца организации." + "message": "В целях обеспечения безопасности вашего аккаунта продолжайте только в том случае, если вы являетесь членом этой организации, у вас включено восстановление аккаунта, а отображаемый ниже отпечаток совпадает с отпечатком организации." }, "trustUser": { "message": "Доверенный пользователь" @@ -10539,7 +10539,7 @@ "message": "Количество назначенных мест превышает количество доступных." }, "userkeyRotationDisclaimerEmergencyAccessText": { - "message": "Фраза отпечатка пальца для контактов $NUM_USERS$, для которых вы включили экстренный доступ.", + "message": "Фраза отпечатка для контактов $NUM_USERS$, которым вы разрешили экстренный доступ.", "placeholders": { "num_users": { "content": "$1", @@ -10548,7 +10548,7 @@ } }, "userkeyRotationDisclaimerAccountRecoveryOrgsText": { - "message": "Фраза отпечатка пальца для организации $ORG_NAME$, для которой вы включили восстановление учетной записи.", + "message": "Фраза отпечатка для организации $ORG_NAME$, которой вы разрешили восстановление аккаунта.", "placeholders": { "org_name": { "content": "$1", @@ -10557,10 +10557,10 @@ } }, "userkeyRotationDisclaimerDescription": { - "message": "Изменение ключей шифрования потребует от вас доверия к ключам любых организаций, которые могут восстановить вашу учетную запись, а также любым контактам, для которых вы включили экстренный доступ. Для продолжения убедитесь, что вы можете проверить следующее:" + "message": "При ротации ключей шифрования вам придется доверить ключи всем организациям, которые могут восстановить ваш аккаунт, и всем контактам, для которых вы включили экстренный доступ. Для продолжения убедитесь, что вы можете проверить следующее:" }, "userkeyRotationDisclaimerTitle": { - "message": "Не доверенные ключи шифрования" + "message": "Недоверенные ключи шифрования" }, "changeAtRiskPassword": { "message": "Изменить пароль, подверженный риску" diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index 834bb36adfa..a6bb37ed1d0 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -6864,11 +6864,11 @@ "message": "Надійний пароль згенеровано! Обов'язково оновіть свій пароль на вебсайті." }, "useGeneratorHelpTextPartOne": { - "message": "Скористатися генератором", + "message": "Скористайтеся генератором,", "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" }, "useGeneratorHelpTextPartTwo": { - "message": "для створення надійного, унікального пароля", + "message": "щоб створити надійний, унікальний пароль", "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" }, "service": { diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index 5140946175f..b3196721a55 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -2912,7 +2912,7 @@ "message": "付款方式" }, "noPaymentMethod": { - "message": "无付款方式。" + "message": "没有付款方式。" }, "addPaymentMethod": { "message": "添加付款方式" @@ -2924,10 +2924,10 @@ "message": "账单" }, "noUnpaidInvoices": { - "message": "无未支付的账单。" + "message": "没有未支付的账单。" }, "noPaidInvoices": { - "message": "无已支付的账单。" + "message": "没有已支付的账单。" }, "paid": { "message": "已支付", @@ -2942,7 +2942,7 @@ "description": "Payment/credit transactions." }, "noTransactions": { - "message": "无交易记录。" + "message": "没有交易记录。" }, "chargeNoun": { "message": "费用", @@ -3968,7 +3968,7 @@ "message": "然后继续创建您的账户。" }, "noEmail": { - "message": "没收到电子邮件吗?" + "message": "没有收到电子邮件吗?" }, "goBack": { "message": "返回" @@ -8956,7 +8956,7 @@ "message": "选择要分配的集合" }, "noCollectionsAssigned": { - "message": "没有分配任何集合" + "message": "未分配任何集合" }, "successfullyAssignedCollections": { "message": "成功分配了集合" @@ -10593,12 +10593,12 @@ "message": "免费组织最多拥有 2 个集合。要添加更多集合,请升级到付费计划。" }, "businessUnit": { - "message": "Business Unit" + "message": "业务单元" }, "businessUnits": { - "message": "Business Units" + "message": "业务单元" }, "newBusinessUnit": { - "message": "New business unit" + "message": "新增业务单元" } } From cb37434a160599e6eb00f620549c841396a998cb Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Tue, 15 Apr 2025 13:40:20 +0200 Subject: [PATCH 18/47] Autosync the updated translations (#14284) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/ar/messages.json | 27 ++++++++++++++++- apps/browser/src/_locales/az/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/be/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/bg/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/bn/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/bs/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/ca/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/cs/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/cy/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/da/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/de/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/el/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/en_GB/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/en_IN/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/es/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/et/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/eu/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/fa/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/fi/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/fil/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/fr/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/gl/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/he/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/hi/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/hr/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/hu/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/id/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/it/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/ja/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/ka/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/km/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/kn/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/ko/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/lt/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/lv/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/ml/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/mr/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/my/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/nb/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/ne/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/nl/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/nn/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/or/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/pl/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/pt_BR/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/pt_PT/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/ro/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/ru/messages.json | 27 ++++++++++++++++- apps/browser/src/_locales/si/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/sk/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/sl/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/sr/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/sv/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/te/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/th/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/tr/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/uk/messages.json | 29 +++++++++++++++++-- apps/browser/src/_locales/vi/messages.json | 25 ++++++++++++++++ apps/browser/src/_locales/zh_CN/messages.json | 29 +++++++++++++++++-- apps/browser/src/_locales/zh_TW/messages.json | 25 ++++++++++++++++ 60 files changed, 1506 insertions(+), 6 deletions(-) diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 3ca64543a4f..1bd6a77b746 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." @@ -3986,7 +4011,7 @@ "message": "رمز غير صحيح" }, "incorrectPin": { - "message": "رمز PIN غير صحيح" + "message": "رمز المرور غير صحيح" }, "multifactorAuthenticationFailed": { "message": "Multifactor authentication failed" diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 07a86b42565..41e6d9b5ab9 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -1098,6 +1098,31 @@ "message": "Giriş məlumatları güncəlləndi", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Saxlama xətası", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 7240371ca2d..f828400c575 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 88128e68be1..457b0693a17 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -1098,6 +1098,31 @@ "message": "Данните за вписване са обновени", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Чудесна работа! Направихте правилните стъпки за Вашата защита и тази на $ORGANIZATION$.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Благодарим Ви, че подобрихте защитата на $ORGANIZATION$. Имате още $TASK_COUNT$ пароли за обновяване.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Промяна на следващата парола", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Грешка при запазването", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index 7a7a9cfa981..275bb18e029 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 0e73dc0b92e..687610c777e 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index 3555642ef90..d9a24c5e473 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index 361d5314e5d..2ed34c8c71a 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -1098,6 +1098,31 @@ "message": "Přihlašovací údaje byly aktualizovány", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Skvělá práce! Podnikli jste kroky, abyste $ORGANIZATION$ udělali bezpečnější.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Děkujeme, že jste udělali $ORGANIZATION$ bezpečnější. Máte $TASK_COUNT$ hesel k aktualizaci.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Změnit další heslo", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Chyba při ukládání", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index c0034df9d2b..eb07358540d 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index c6f051ce811..914c74411e4 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 274bc934b39..0edf5edfa3d 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -1098,6 +1098,31 @@ "message": "Zugangsdaten aktualisiert", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Fehler beim Speichern", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index afd76596c85..1c15f70d40c 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index 3b968f11587..520af1ca8ff 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index f32abc028ed..f75664f10db 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index e031a4487d6..1400cb78845 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 670dab1fcda..f9f7cb950fe 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index b5ffbf45ba2..ec1f5189946 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index d8a8f827db4..0e5e8943b7d 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 56e25aead7d..4048925ebaa 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -1098,6 +1098,31 @@ "message": "Kirjautumistieto päivitettiin", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Virhe tallennettaessa", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 69d5895ba27..9c3865bc0da 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index fe2127148c8..1f05f699ea9 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -1098,6 +1098,31 @@ "message": "Identifiant mis à jour", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Erreur lors de l'enregistrement", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 83af8b6db75..b2b843d7830 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index 605dd5454f0..fd38df53906 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -1098,6 +1098,31 @@ "message": "כניסה עודכנה", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "שגיאה בשמירה", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 9cc0f5251e6..813b69b9079 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index bef7c9b8f13..896593332a4 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -1098,6 +1098,31 @@ "message": "Prijava ažurirana", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Greška kod spremanja", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index d0114b30b7b..f58e542c77f 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -1098,6 +1098,31 @@ "message": "A bejelentkezés frissítésre került.", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Remek munka! Megtettük a lépéseket, hogy magunk és $ORGANIZATION$ biztonságosabbá váljon.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Köszönet $ORGANIZATION$ biztonságosabbá válásához. További $TASK_COUNT$ frissítendő jelszó van még.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Következő jelszó megváltoztatása", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Hiba történt a bejelentkezés mentésekor.", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 16b11c9e732..101fcf43795 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -1098,6 +1098,31 @@ "message": "Log masuk diperbarui", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Kesalahan saat menyimpan", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 1448776e430..5f9a6ca7dca 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -1098,6 +1098,31 @@ "message": "Accesso aggiornato", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Errore nel salvataggio", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 4aae076174e..34b15699437 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -1098,6 +1098,31 @@ "message": "ログイン情報が更新されました", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "保存エラー", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index 132a9b4d4ed..f261f69904d 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index a9f5f5c2d75..7bec167e1c6 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 9e137fd6156..7d9583dc652 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index dfc7364710a..7d65686df5d 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 93bbbef7292..2245fdb0029 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index a42ee2ace78..517dacfb72b 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -1098,6 +1098,31 @@ "message": "Pieteikšanās vienums atjaunināts", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Lielisks darbs! Tu attiecīgi rīkojies, lai padarītu savi un $ORGANIZATION$ drošāku.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Paldies par $ORGANIZATION$ padarīšanu drošāku! Tev ir vēl $TASK_COUNT$ paroļu, ko atjaunināt.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Mainīt nākamo paroli", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Kļūda saglabāšanas laikā", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index 5810d5392e6..49a2695ce6f 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index 303a2cf96dd..6b2f7b8bc32 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index a9f5f5c2d75..7bec167e1c6 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 18d00d9c274..7fa41eed68b 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index a9f5f5c2d75..7bec167e1c6 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index 80eba21ddb3..dd101e62e75 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -1098,6 +1098,31 @@ "message": "Login bijgewerkt", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Goed gedaan! Je hebt stappen genomen om jou en $ORGANIZATION$ veiliger te maken.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Bedankt voor het veiliger maken van $ORGANIZATION$. Je moet nog $TASK_COUNT$ wachtwoorden bijwerken.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Volgend wachtwoord veranderen", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Fout bij opslaan", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index a9f5f5c2d75..7bec167e1c6 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index a9f5f5c2d75..7bec167e1c6 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 5e74025800e..3d25e6f123c 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -1098,6 +1098,31 @@ "message": "Dane logowania zostały zaktualizowane", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Świetna robota! Podjęto kroki mające na celu zwiększenie bezpieczeństwa Twojego oraz $ORGANIZATION$.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Dziękujemy za dbanie o bezpieczeństwo $ORGANIZATION$. Pozostało $TASK_COUNT$ haseł do zaktualizowania.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Zmień następne hasło", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Błąd podczas zapisywania", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index f22958d0498..9d1744f5473 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -1098,6 +1098,31 @@ "message": "Sessão atualizada", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Erro ao salvar", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 172e748a3f1..daab7fb05d9 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -1098,6 +1098,31 @@ "message": "Credencial atualizada", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Excelente trabalho! Tomou as medidas necessárias para o tornar a si e à sua $ORGANIZATION$ mais seguros.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Obrigado por tornar a $ORGANIZATION$ mais segura. Tem mais $TASK_COUNT$ palavras-passe para atualizar.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Alterar a palavra-passe seguinte", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Erro ao guardar", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 0272add140a..bcdf10f37f1 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index 639a3b740db..b0f03d34f69 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -1098,6 +1098,31 @@ "message": "Логин обновлен", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Отличная работа! Вы предприняли шаги, чтобы сделать себя и $ORGANIZATION$ более защищенными.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Спасибо, что сделали $ORGANIZATION$ более защищенной. У вас есть еще $TASK_COUNT$ паролей для обновления.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Изменить следующий пароль", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Ошибка при сохранении", "description": "Error message shown when the system fails to save login details." @@ -1772,7 +1797,7 @@ "message": "Полное имя" }, "identityName": { - "message": "Имя" + "message": "Название личности" }, "company": { "message": "Компания" diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index 70c1ff7126a..4efbb2dceeb 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index 0b0a41a49b2..b427e902334 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -1098,6 +1098,31 @@ "message": "Prihlasovacie údaje aktualizované", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Skvelá práca! Podnikli ste kroky, aby ste vy a $ORGANIZATION$ boli bezpečnejší.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Ďakujeme, že ste pre $ORGANIZATION$ zabezpečili väčšiu bezpečnosť. Máte ďalšie ($TASK_COUNT$) heslá na aktualizáciu.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Zmeniť ďalšie heslo", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Chyba pri ukladaní", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 0fc3b26acb8..61a70650d16 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index c402372895e..a36006894ba 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -1098,6 +1098,31 @@ "message": "Пријава ажурирана", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Грешка при снимању", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 74b9bbccc80..54a34e15013 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index a9f5f5c2d75..7bec167e1c6 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 0ef98835f18..b502e29cfdd 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index f009a52a951..8a00287dfcc 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -1098,6 +1098,31 @@ "message": "Hesap güncellendi", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Kaydetme hatası", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index 11e5b304611..bdd96ba9ea4 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -1098,6 +1098,31 @@ "message": "Запис оновлено", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Чудова робота! Ви щойно підвищили рівень безпеки для себе та $ORGANIZATION$.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Дякуємо за підвищення рівня безпеки $ORGANIZATION$. Залишилося оновити ще $TASK_COUNT$ паролів.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Змінити наступний пароль", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Помилка збереження", "description": "Error message shown when the system fails to save login details." @@ -2126,11 +2151,11 @@ "message": "Надійний пароль згенеровано! Обов'язково оновіть свій пароль на вебсайті." }, "useGeneratorHelpTextPartOne": { - "message": "Скористатися генератором", + "message": "Скористайтеся генератором,", "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" }, "useGeneratorHelpTextPartTwo": { - "message": "для створення надійного, унікального пароля", + "message": "щоб створити надійний, унікальний пароль", "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" }, "vaultCustomization": { diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 4693c93af9c..c6c6ed7ee6d 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index c8eaacc8c86..662284568cd 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -1098,6 +1098,31 @@ "message": "登录已更新", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "干得好!您采取了使您和 $ORGANIZATION$ 更加安全的措施。", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "感谢您使 $ORGANIZATION$ 更加安全。您还有 $TASK_COUNT$ 个密码需要更新。", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "更改下一个密码", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "保存时出错", "description": "Error message shown when the system fails to save login details." @@ -2825,7 +2850,7 @@ "message": "选择文件夹..." }, "noFoldersFound": { - "message": "找不到文件夹", + "message": "未找到任何文件夹", "description": "Used as a message within the notification bar when no folders are found" }, "orgPermissionsUpdatedMustSetPassword": { @@ -3929,7 +3954,7 @@ "message": "此应用程序已存在一个通行密钥。" }, "noPasskeysFoundForThisApplication": { - "message": "没有找到此应用程序的通行密钥。" + "message": "未找到此应用程序的通行密钥。" }, "noMatchingPasskeyLogin": { "message": "您没有匹配此站点的登录。" diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index 1ba3a2166dd..7d56ecd8e63 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." From 9f174e7723ebdd39f4351cf434ac45d7cbc213f6 Mon Sep 17 00:00:00 2001 From: Opeyemi Date: Tue, 15 Apr 2025 14:57:37 +0100 Subject: [PATCH 19/47] update mas-dev SHA-1 (#14290) --- apps/browser/scripts/package-safari.ps1 | 2 +- apps/desktop/scripts/after-pack.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/browser/scripts/package-safari.ps1 b/apps/browser/scripts/package-safari.ps1 index ce208478098..1df40c68b37 100755 --- a/apps/browser/scripts/package-safari.ps1 +++ b/apps/browser/scripts/package-safari.ps1 @@ -52,7 +52,7 @@ foreach ($subBuildPath in $subBuildPaths) { "--verbose", "--force", "--sign", - "4B9662CAB74E8E4F4ECBDD9EDEF2543659D95E3C", + "588E3F1724AE018EBA762E42279DAE85B313E3ED", "--entitlements", $entitlementsPath ) diff --git a/apps/desktop/scripts/after-pack.js b/apps/desktop/scripts/after-pack.js index 45b79c0fe86..99c3d91be52 100644 --- a/apps/desktop/scripts/after-pack.js +++ b/apps/desktop/scripts/after-pack.js @@ -42,7 +42,7 @@ async function run(context) { if (process.env.GITHUB_ACTIONS === "true") { if (is_mas) { id = is_mas_dev - ? "4B9662CAB74E8E4F4ECBDD9EDEF2543659D95E3C" + ? "588E3F1724AE018EBA762E42279DAE85B313E3ED" : "3rd Party Mac Developer Application: Bitwarden Inc"; } else { id = "Developer ID Application: 8bit Solutions LLC"; From b09305577f450922fe0a37f3ee35e756eb603eb8 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 15 Apr 2025 16:39:02 +0200 Subject: [PATCH 20/47] [PM-19603] Change asymmetric interface to only allow key encapsulation (#14046) * Change asymmetric interface to only allow key encapsulation * Fix naming * Clean up naming * Update libs/common/src/key-management/crypto/abstractions/encrypt.service.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/crypto/abstractions/encrypt.service.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Fix test --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> --- .../bulk/base-bulk-confirm.component.ts | 2 +- .../members/members.component.ts | 2 +- ...zation-user-reset-password.service.spec.ts | 9 +- ...rganization-user-reset-password.service.ts | 12 ++- .../rotateable-key-set.service.spec.ts | 2 +- .../services/rotateable-key-set.service.ts | 10 ++- .../services/emergency-access.service.spec.ts | 10 ++- .../services/emergency-access.service.ts | 14 ++-- .../accept-organization.service.spec.ts | 6 +- .../accept-organization.service.ts | 2 +- .../organization-auth-request.service.spec.ts | 13 ++- .../organization-auth-request.service.ts | 6 +- .../providers/manage/members.component.ts | 2 +- .../auth/components/set-password.component.ts | 5 +- .../default-set-password-jit.service.spec.ts | 4 +- .../default-set-password-jit.service.ts | 2 +- .../webauthn-login.strategy.spec.ts | 12 +-- .../webauthn-login.strategy.ts | 5 +- .../auth-request/auth-request.service.spec.ts | 32 ++++--- .../auth-request/auth-request.service.ts | 20 ++--- .../domain/encrypted-organization-key.ts | 6 +- ...-enrollment.service.implementation.spec.ts | 4 +- ...reset-enrollment.service.implementation.ts | 2 +- .../crypto/abstractions/encrypt.service.ts | 31 +++++++ .../encrypt.service.implementation.ts | 84 +++++++++++-------- .../crypto/services/encrypt.service.spec.ts | 37 ++++---- .../device-trust.service.implementation.ts | 10 +-- .../services/device-trust.service.spec.ts | 18 ++-- libs/key-management/src/key.service.spec.ts | 4 +- libs/key-management/src/key.service.ts | 6 +- 30 files changed, 229 insertions(+), 143 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/base-bulk-confirm.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/base-bulk-confirm.component.ts index 05e302f011d..6b3f958fe51 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/base-bulk-confirm.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/base-bulk-confirm.component.ts @@ -71,7 +71,7 @@ export abstract class BaseBulkConfirmComponent implements OnInit { if (publicKey == null) { continue; } - const encryptedKey = await this.encryptService.rsaEncrypt(key.key, publicKey); + const encryptedKey = await this.encryptService.encapsulateKeyUnsigned(key, publicKey); userIdsWithKeys.push({ id: user.id, key: encryptedKey.encryptedString, diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.ts b/apps/web/src/app/admin-console/organizations/members/members.component.ts index a64247339a5..3ce78354046 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.ts @@ -322,7 +322,7 @@ export class MembersComponent extends BaseMembersComponent async confirmUser(user: OrganizationUserView, publicKey: Uint8Array): Promise { const orgKey = await this.keyService.getOrgKey(this.organization.id); - const key = await this.encryptService.rsaEncrypt(orgKey.key, publicKey); + const key = await this.encryptService.encapsulateKeyUnsigned(orgKey, publicKey); const request = new OrganizationUserConfirmRequest(); request.key = key.encryptedString; await this.organizationUserApiService.postOrganizationUserConfirm( diff --git a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.spec.ts b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.spec.ts index ce167950727..575c1017f21 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.spec.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.spec.ts @@ -89,7 +89,7 @@ describe("OrganizationUserResetPasswordService", () => { }), ); - encryptService.rsaEncrypt.mockResolvedValue( + encryptService.encapsulateKeyUnsigned.mockResolvedValue( new EncString(EncryptionType.Rsa2048_OaepSha1_B64, "mockEncryptedUserKey"), ); }); @@ -111,7 +111,10 @@ describe("OrganizationUserResetPasswordService", () => { it("should rsa encrypt the user key", async () => { await sut.buildRecoveryKey(mockOrgId, mockUserKey, mockPublicKeys); - expect(encryptService.rsaEncrypt).toHaveBeenCalledWith(expect.anything(), expect.anything()); + expect(encryptService.encapsulateKeyUnsigned).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + ); }); it("should throw an error if the public key is not trusted", async () => { @@ -199,7 +202,7 @@ describe("OrganizationUserResetPasswordService", () => { publicKey: Utils.fromUtf8ToArray("test-public-key"), }), ); - encryptService.rsaEncrypt.mockResolvedValue( + encryptService.encapsulateKeyUnsigned.mockResolvedValue( new EncString(EncryptionType.Rsa2048_OaepSha1_B64, "mockEncryptedUserKey"), ); }); diff --git a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts index 4b5c03a5a5b..78d2d8fd165 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts @@ -14,7 +14,6 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; -import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { UserId } from "@bitwarden/common/types/guid"; import { UserKey } from "@bitwarden/common/types/key"; import { @@ -59,6 +58,10 @@ export class OrganizationUserResetPasswordService userKey: UserKey, trustedPublicKeys: Uint8Array[], ): Promise { + if (userKey == null) { + throw new Error("User key is required for recovery."); + } + // Retrieve Public Key const orgKeys = await this.organizationApiService.getKeys(orgId); if (orgKeys == null) { @@ -76,7 +79,8 @@ export class OrganizationUserResetPasswordService } // RSA Encrypt user key with organization's public key - const encryptedKey = await this.encryptService.rsaEncrypt(userKey.key, publicKey); + const encryptedKey = await this.encryptService.encapsulateKeyUnsigned(userKey, publicKey); + return encryptedKey.encryptedString; } @@ -114,11 +118,11 @@ export class OrganizationUserResetPasswordService ); // Decrypt User's Reset Password Key to get UserKey - const decValue = await this.encryptService.rsaDecrypt( + const userKey = await this.encryptService.decapsulateKeyUnsigned( new EncString(response.resetPasswordKey), decPrivateKey, ); - const existingUserKey = new SymmetricCryptoKey(decValue) as UserKey; + const existingUserKey = userKey as UserKey; // determine Kdf Algorithm const kdfConfig: KdfConfig = diff --git a/apps/web/src/app/auth/core/services/rotateable-key-set.service.spec.ts b/apps/web/src/app/auth/core/services/rotateable-key-set.service.spec.ts index 1241ea88fe9..1a83fed37b7 100644 --- a/apps/web/src/app/auth/core/services/rotateable-key-set.service.spec.ts +++ b/apps/web/src/app/auth/core/services/rotateable-key-set.service.spec.ts @@ -35,7 +35,7 @@ describe("RotateableKeySetService", () => { const encryptedPrivateKey = Symbol(); keyService.makeKeyPair.mockResolvedValue(["publicKey", encryptedPrivateKey as any]); keyService.getUserKey.mockResolvedValue({ key: userKey.key } as any); - encryptService.rsaEncrypt.mockResolvedValue(encryptedUserKey as any); + encryptService.encapsulateKeyUnsigned.mockResolvedValue(encryptedUserKey as any); encryptService.encrypt.mockResolvedValue(encryptedPublicKey as any); const result = await service.createKeySet(externalKey as any); diff --git a/apps/web/src/app/auth/core/services/rotateable-key-set.service.ts b/apps/web/src/app/auth/core/services/rotateable-key-set.service.ts index 64a8bdfbfb4..8510aa1c29a 100644 --- a/apps/web/src/app/auth/core/services/rotateable-key-set.service.ts +++ b/apps/web/src/app/auth/core/services/rotateable-key-set.service.ts @@ -25,7 +25,10 @@ export class RotateableKeySetService { const userKey = await this.keyService.getUserKey(); const rawPublicKey = Utils.fromB64ToArray(publicKey); - const encryptedUserKey = await this.encryptService.rsaEncrypt(userKey.key, rawPublicKey); + const encryptedUserKey = await this.encryptService.encapsulateKeyUnsigned( + userKey, + rawPublicKey, + ); const encryptedPublicKey = await this.encryptService.encrypt(rawPublicKey, userKey); return new RotateableKeySet(encryptedUserKey, encryptedPublicKey, encryptedPrivateKey); } @@ -60,7 +63,10 @@ export class RotateableKeySetService { throw new Error("failed to rotate key set: could not decrypt public key"); } const newEncryptedPublicKey = await this.encryptService.encrypt(publicKey, newUserKey); - const newEncryptedUserKey = await this.encryptService.rsaEncrypt(newUserKey.key, publicKey); + const newEncryptedUserKey = await this.encryptService.encapsulateKeyUnsigned( + newUserKey, + publicKey, + ); const newRotateableKeySet = new RotateableKeySet( newEncryptedUserKey, 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 6ad2c4de70e..6b4b3f2ff1e 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 @@ -130,7 +130,9 @@ describe("EmergencyAccessService", () => { keyService.getUserKey.mockResolvedValueOnce(mockUserKey); - encryptService.rsaEncrypt.mockResolvedValueOnce(mockUserPublicKeyEncryptedUserKey); + encryptService.encapsulateKeyUnsigned.mockResolvedValueOnce( + mockUserPublicKeyEncryptedUserKey, + ); emergencyAccessApiService.postEmergencyAccessConfirm.mockResolvedValueOnce(); @@ -160,7 +162,9 @@ describe("EmergencyAccessService", () => { const mockDecryptedGrantorUserKey = new Uint8Array(64); keyService.getPrivateKey.mockResolvedValue(new Uint8Array(64)); - encryptService.rsaDecrypt.mockResolvedValueOnce(mockDecryptedGrantorUserKey); + encryptService.decapsulateKeyUnsigned.mockResolvedValueOnce( + new SymmetricCryptoKey(mockDecryptedGrantorUserKey), + ); const mockMasterKey = new SymmetricCryptoKey(new Uint8Array(64) as CsprngArray) as MasterKey; @@ -253,7 +257,7 @@ describe("EmergencyAccessService", () => { publicKey: Utils.fromUtf8ToB64("trustedPublicKey"), } as UserKeyResponse); - encryptService.rsaEncrypt.mockImplementation((plainValue, publicKey) => { + encryptService.encapsulateKeyUnsigned.mockImplementation((plainValue, publicKey) => { return Promise.resolve( new EncString(EncryptionType.Rsa2048_OaepSha1_B64, "Encrypted: " + plainValue), ); 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 e86e0822ef3..5094c0c09ab 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 @@ -12,7 +12,6 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; -import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { UserId } from "@bitwarden/common/types/guid"; import { UserKey } from "@bitwarden/common/types/key"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -234,11 +233,10 @@ export class EmergencyAccessService throw new Error("Active user does not have a private key, cannot get view only ciphers."); } - const grantorKeyBuffer = await this.encryptService.rsaDecrypt( + const grantorUserKey = (await this.encryptService.decapsulateKeyUnsigned( new EncString(response.keyEncrypted), activeUserPrivateKey, - ); - const grantorUserKey = new SymmetricCryptoKey(grantorKeyBuffer) as UserKey; + )) as UserKey; let ciphers: CipherView[] = []; if (await this.configService.getFeatureFlag(FeatureFlag.PM4154_BulkEncryptionService)) { @@ -271,15 +269,15 @@ export class EmergencyAccessService throw new Error("Active user does not have a private key, cannot complete a takeover."); } - const grantorKeyBuffer = await this.encryptService.rsaDecrypt( + const grantorKey = await this.encryptService.decapsulateKeyUnsigned( new EncString(takeoverResponse.keyEncrypted), activeUserPrivateKey, ); - if (grantorKeyBuffer == null) { + if (grantorKey == null) { throw new Error("Failed to decrypt grantor key"); } - const grantorUserKey = new SymmetricCryptoKey(grantorKeyBuffer) as UserKey; + const grantorUserKey = grantorKey as UserKey; let config: KdfConfig; @@ -407,6 +405,6 @@ export class EmergencyAccessService } private async encryptKey(userKey: UserKey, publicKey: Uint8Array): Promise { - return (await this.encryptService.rsaEncrypt(userKey.key, publicKey)).encryptedString; + return (await this.encryptService.encapsulateKeyUnsigned(userKey, publicKey)).encryptedString; } } diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts b/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts index e7137e1d5bb..cc2d0e371ff 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts +++ b/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts @@ -196,7 +196,7 @@ describe("AcceptOrganizationInviteService", () => { ); accountService.activeAccount$ = new BehaviorSubject({ id: "activeUserId" }) as any; keyService.userKey$.mockReturnValue(new BehaviorSubject({ key: "userKey" } as any)); - encryptService.rsaEncrypt.mockResolvedValue({ + encryptService.encapsulateKeyUnsigned.mockResolvedValue({ encryptedString: "encryptedString", } as EncString); @@ -218,8 +218,8 @@ describe("AcceptOrganizationInviteService", () => { expect(result).toBe(true); expect(OrganizationTrustComponent.open).toHaveBeenCalled(); - expect(encryptService.rsaEncrypt).toHaveBeenCalledWith( - "userKey", + expect(encryptService.encapsulateKeyUnsigned).toHaveBeenCalledWith( + { key: "userKey" }, Utils.fromB64ToArray("publicKey"), ); expect(organizationUserApiService.postOrganizationUserAccept).toHaveBeenCalled(); diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.service.ts b/apps/web/src/app/auth/organization-invite/accept-organization.service.ts index 837031380f3..8b5db9f4872 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.service.ts +++ b/apps/web/src/app/auth/organization-invite/accept-organization.service.ts @@ -202,7 +202,7 @@ export class AcceptOrganizationInviteService { const activeUserId = (await firstValueFrom(this.accountService.activeAccount$)).id; const userKey = await firstValueFrom(this.keyService.userKey$(activeUserId)); // RSA Encrypt user's encKey.key with organization public key - const encryptedKey = await this.encryptService.rsaEncrypt(userKey.key, publicKey); + const encryptedKey = await this.encryptService.encapsulateKeyUnsigned(userKey, publicKey); // Add reset password key to accept request request.resetPasswordKey = encryptedKey.encryptedString; diff --git a/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.spec.ts b/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.spec.ts index 448399a8bb0..933a5af1760 100644 --- a/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.spec.ts +++ b/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.spec.ts @@ -7,6 +7,7 @@ import { import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { KeyService } from "@bitwarden/key-management"; import { OrganizationAuthRequestApiService } from "./organization-auth-request-api.service"; @@ -124,8 +125,10 @@ describe("OrganizationAuthRequestService", () => { ); const encryptedUserKey = new EncString("encryptedUserKey"); - encryptService.rsaDecrypt.mockResolvedValue(new Uint8Array(32)); - encryptService.rsaEncrypt.mockResolvedValue(encryptedUserKey); + encryptService.decapsulateKeyUnsigned.mockResolvedValue( + new SymmetricCryptoKey(new Uint8Array(32)), + ); + encryptService.encapsulateKeyUnsigned.mockResolvedValue(encryptedUserKey); const mockPendingAuthRequest = new PendingAuthRequestView(); mockPendingAuthRequest.id = "requestId1"; @@ -166,8 +169,10 @@ describe("OrganizationAuthRequestService", () => { ); const encryptedUserKey = new EncString("encryptedUserKey"); - encryptService.rsaDecrypt.mockResolvedValue(new Uint8Array(32)); - encryptService.rsaEncrypt.mockResolvedValue(encryptedUserKey); + encryptService.decapsulateKeyUnsigned.mockResolvedValue( + new SymmetricCryptoKey(new Uint8Array(32)), + ); + encryptService.encapsulateKeyUnsigned.mockResolvedValue(encryptedUserKey); const mockPendingAuthRequest = new PendingAuthRequestView(); mockPendingAuthRequest.id = "requestId1"; diff --git a/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.ts b/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.ts index 025b021f83d..97e271e770e 100644 --- a/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.ts +++ b/bitwarden_license/bit-common/src/admin-console/auth-requests/organization-auth-request.service.ts @@ -7,7 +7,6 @@ import { import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; -import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { KeyService } from "@bitwarden/key-management"; import { OrganizationAuthRequestApiService } from "./organization-auth-request-api.service"; @@ -119,13 +118,12 @@ export class OrganizationAuthRequestService { ); // Decrypt user key with decrypted org private key - const decValue = await this.encryptService.rsaDecrypt( + const userKey = await this.encryptService.decapsulateKeyUnsigned( new EncString(encryptedUserKey), decOrgPrivateKey, ); - const userKey = new SymmetricCryptoKey(decValue); // Re-encrypt user Key with the Device Public Key - return await this.encryptService.rsaEncrypt(userKey.key, devicePubKey); + return await this.encryptService.encapsulateKeyUnsigned(userKey, devicePubKey); } } diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts index 7eff2c0f0b6..4a184d2dd16 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.ts @@ -187,7 +187,7 @@ export class MembersComponent extends BaseMembersComponent { async confirmUser(user: ProviderUser, publicKey: Uint8Array): Promise { const providerKey = await this.keyService.getProviderKey(this.providerId); - const key = await this.encryptService.rsaEncrypt(providerKey.key, publicKey); + const key = await this.encryptService.encapsulateKeyUnsigned(providerKey, publicKey); const request = new ProviderUserConfirmRequest(); request.key = key.encryptedString; await this.apiService.postProviderUserConfirm(this.providerId, user.id, request); diff --git a/libs/angular/src/auth/components/set-password.component.ts b/libs/angular/src/auth/components/set-password.component.ts index aec0673bb52..ee0756355cf 100644 --- a/libs/angular/src/auth/components/set-password.component.ts +++ b/libs/angular/src/auth/components/set-password.component.ts @@ -211,7 +211,10 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements // RSA Encrypt user key with organization public key const userKey = await this.keyService.getUserKey(); - const encryptedUserKey = await this.encryptService.rsaEncrypt(userKey.key, publicKey); + const encryptedUserKey = await this.encryptService.encapsulateKeyUnsigned( + userKey, + publicKey, + ); const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest(); resetRequest.masterPasswordHash = masterPasswordHash; diff --git a/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.spec.ts b/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.spec.ts index cbcebd14526..12d4d8a2e39 100644 --- a/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.spec.ts +++ b/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.spec.ts @@ -174,7 +174,7 @@ describe("DefaultSetPasswordJitService", () => { } keyService.userKey$.mockReturnValue(of(userKey)); - encryptService.rsaEncrypt.mockResolvedValue(userKeyEncString); + encryptService.encapsulateKeyUnsigned.mockResolvedValue(userKeyEncString); organizationUserApiService.putOrganizationUserResetPasswordEnrollment.mockResolvedValue( undefined, @@ -216,7 +216,7 @@ describe("DefaultSetPasswordJitService", () => { // Assert expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); expect(organizationApiService.getKeys).toHaveBeenCalledWith(orgId); - expect(encryptService.rsaEncrypt).toHaveBeenCalledWith(userKey.key, orgPublicKey); + expect(encryptService.encapsulateKeyUnsigned).toHaveBeenCalledWith(userKey, orgPublicKey); expect( organizationUserApiService.putOrganizationUserResetPasswordEnrollment, ).toHaveBeenCalled(); diff --git a/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.ts b/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.ts index 174760aae21..2f96972d88d 100644 --- a/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.ts +++ b/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.ts @@ -161,7 +161,7 @@ export class DefaultSetPasswordJitService implements SetPasswordJitService { throw new Error("userKey not found. Could not handle reset password auto enroll."); } - const encryptedUserKey = await this.encryptService.rsaEncrypt(userKey.key, publicKey); + const encryptedUserKey = await this.encryptService.encapsulateKeyUnsigned(userKey, publicKey); const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest(); resetRequest.masterPasswordHash = masterKeyHash; diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts index aac4a36c24a..2cdeb710ab9 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts @@ -231,7 +231,9 @@ describe("WebAuthnLoginStrategy", () => { const mockUserKey = new SymmetricCryptoKey(mockUserKeyArray) as UserKey; encryptService.decryptToBytes.mockResolvedValue(mockPrfPrivateKey); - encryptService.rsaDecrypt.mockResolvedValue(mockUserKeyArray); + encryptService.decapsulateKeyUnsigned.mockResolvedValue( + new SymmetricCryptoKey(mockUserKeyArray), + ); // Act await webAuthnLoginStrategy.logIn(webAuthnCredentials); @@ -249,8 +251,8 @@ describe("WebAuthnLoginStrategy", () => { idTokenResponse.userDecryptionOptions.webAuthnPrfOption.encryptedPrivateKey, webAuthnCredentials.prfKey, ); - expect(encryptService.rsaDecrypt).toHaveBeenCalledTimes(1); - expect(encryptService.rsaDecrypt).toHaveBeenCalledWith( + expect(encryptService.decapsulateKeyUnsigned).toHaveBeenCalledTimes(1); + expect(encryptService.decapsulateKeyUnsigned).toHaveBeenCalledWith( idTokenResponse.userDecryptionOptions.webAuthnPrfOption.encryptedUserKey, mockPrfPrivateKey, ); @@ -278,7 +280,7 @@ describe("WebAuthnLoginStrategy", () => { // Assert expect(encryptService.decryptToBytes).not.toHaveBeenCalled(); - expect(encryptService.rsaDecrypt).not.toHaveBeenCalled(); + expect(encryptService.decapsulateKeyUnsigned).not.toHaveBeenCalled(); expect(keyService.setUserKey).not.toHaveBeenCalled(); }); @@ -330,7 +332,7 @@ describe("WebAuthnLoginStrategy", () => { apiService.postIdentityToken.mockResolvedValue(idTokenResponse); - encryptService.rsaDecrypt.mockResolvedValue(null); + encryptService.decapsulateKeyUnsigned.mockResolvedValue(null); // Act await webAuthnLoginStrategy.logIn(webAuthnCredentials); diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts index 97696a26699..895617b3237 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts @@ -7,7 +7,6 @@ import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { WebAuthnLoginTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/webauthn-login-token.request"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; -import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { UserId } from "@bitwarden/common/types/guid"; import { UserKey } from "@bitwarden/common/types/key"; @@ -89,13 +88,13 @@ export class WebAuthnLoginStrategy extends LoginStrategy { ); // decrypt user key with private key - const userKey = await this.encryptService.rsaDecrypt( + const userKey = await this.encryptService.decapsulateKeyUnsigned( new EncString(webAuthnPrfOption.encryptedUserKey.encryptedString), privateKey, ); if (userKey) { - await this.keyService.setUserKey(new SymmetricCryptoKey(userKey) as UserKey, userId); + await this.keyService.setUserKey(userKey as UserKey, userId); } } } diff --git a/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts b/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts index 1e9c46db0ee..b5846fcfdbf 100644 --- a/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts +++ b/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts @@ -88,6 +88,9 @@ describe("AuthRequestService", () => { encryptService.rsaEncrypt.mockResolvedValue({ encryptedString: "ENCRYPTED_STRING", } as EncString); + encryptService.encapsulateKeyUnsigned.mockResolvedValue({ + encryptedString: "ENCRYPTED_STRING", + } as EncString); appIdService.getAppId.mockResolvedValue("APP_ID"); }); it("should throw if auth request is missing id or key", async () => { @@ -111,7 +114,10 @@ describe("AuthRequestService", () => { new AuthRequestResponse({ id: "123", publicKey: "KEY" }), ); - expect(encryptService.rsaEncrypt).toHaveBeenCalledWith(new Uint8Array(64), expect.anything()); + expect(encryptService.encapsulateKeyUnsigned).toHaveBeenCalledWith( + { encKey: new Uint8Array(64) }, + expect.anything(), + ); }); it("should use the user key if the master key and hash do not exist", async () => { @@ -122,7 +128,10 @@ describe("AuthRequestService", () => { new AuthRequestResponse({ id: "123", publicKey: "KEY" }), ); - expect(encryptService.rsaEncrypt).toHaveBeenCalledWith(new Uint8Array(64), expect.anything()); + expect(encryptService.encapsulateKeyUnsigned).toHaveBeenCalledWith( + { key: new Uint8Array(64) }, + expect.anything(), + ); }); }); describe("setUserKeyAfterDecryptingSharedUserKey", () => { @@ -214,7 +223,9 @@ describe("AuthRequestService", () => { const mockDecryptedUserKeyBytes = new Uint8Array(64); const mockDecryptedUserKey = new SymmetricCryptoKey(mockDecryptedUserKeyBytes) as UserKey; - encryptService.rsaDecrypt.mockResolvedValueOnce(mockDecryptedUserKeyBytes); + encryptService.decapsulateKeyUnsigned.mockResolvedValueOnce( + new SymmetricCryptoKey(mockDecryptedUserKeyBytes), + ); // Act const result = await sut.decryptPubKeyEncryptedUserKey( @@ -223,7 +234,7 @@ describe("AuthRequestService", () => { ); // Assert - expect(encryptService.rsaDecrypt).toBeCalledWith( + expect(encryptService.decapsulateKeyUnsigned).toBeCalledWith( new EncString(mockPubKeyEncryptedUserKey), mockPrivateKey, ); @@ -244,9 +255,10 @@ describe("AuthRequestService", () => { const mockDecryptedMasterKeyHashBytes = new Uint8Array(64); const mockDecryptedMasterKeyHash = Utils.fromBufferToUtf8(mockDecryptedMasterKeyHashBytes); - encryptService.rsaDecrypt - .mockResolvedValueOnce(mockDecryptedMasterKeyBytes) - .mockResolvedValueOnce(mockDecryptedMasterKeyHashBytes); + encryptService.rsaDecrypt.mockResolvedValueOnce(mockDecryptedMasterKeyHashBytes); + encryptService.decapsulateKeyUnsigned.mockResolvedValueOnce( + new SymmetricCryptoKey(mockDecryptedMasterKeyBytes), + ); // Act const result = await sut.decryptPubKeyEncryptedMasterKeyAndHash( @@ -256,13 +268,11 @@ describe("AuthRequestService", () => { ); // Assert - expect(encryptService.rsaDecrypt).toHaveBeenNthCalledWith( - 1, + expect(encryptService.decapsulateKeyUnsigned).toHaveBeenCalledWith( new EncString(mockPubKeyEncryptedMasterKey), mockPrivateKey, ); - expect(encryptService.rsaDecrypt).toHaveBeenNthCalledWith( - 2, + expect(encryptService.rsaDecrypt).toHaveBeenCalledWith( new EncString(mockPubKeyEncryptedMasterKeyHash), mockPrivateKey, ); diff --git a/libs/auth/src/common/services/auth-request/auth-request.service.ts b/libs/auth/src/common/services/auth-request/auth-request.service.ts index 1da5d2f1882..f4316c2e519 100644 --- a/libs/auth/src/common/services/auth-request/auth-request.service.ts +++ b/libs/auth/src/common/services/auth-request/auth-request.service.ts @@ -14,7 +14,6 @@ import { AuthRequestPushNotification } from "@bitwarden/common/models/response/n import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; -import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { AUTH_REQUEST_DISK_LOCAL, StateProvider, @@ -116,13 +115,12 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { Utils.fromUtf8ToArray(masterKeyHash), pubKey, ); - keyToEncrypt = masterKey.encKey; + keyToEncrypt = masterKey; } else { - const userKey = await this.keyService.getUserKey(); - keyToEncrypt = userKey.key; + keyToEncrypt = await this.keyService.getUserKey(); } - const encryptedKey = await this.encryptService.rsaEncrypt(keyToEncrypt, pubKey); + const encryptedKey = await this.encryptService.encapsulateKeyUnsigned(keyToEncrypt, pubKey); const response = new PasswordlessAuthRequest( encryptedKey.encryptedString, @@ -171,12 +169,10 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { pubKeyEncryptedUserKey: string, privateKey: Uint8Array, ): Promise { - const decryptedUserKeyBytes = await this.encryptService.rsaDecrypt( + return (await this.encryptService.decapsulateKeyUnsigned( new EncString(pubKeyEncryptedUserKey), privateKey, - ); - - return new SymmetricCryptoKey(decryptedUserKeyBytes) as UserKey; + )) as UserKey; } async decryptPubKeyEncryptedMasterKeyAndHash( @@ -184,17 +180,15 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { pubKeyEncryptedMasterKeyHash: string, privateKey: Uint8Array, ): Promise<{ masterKey: MasterKey; masterKeyHash: string }> { - const decryptedMasterKeyArrayBuffer = await this.encryptService.rsaDecrypt( + const masterKey = (await this.encryptService.decapsulateKeyUnsigned( new EncString(pubKeyEncryptedMasterKey), privateKey, - ); + )) as MasterKey; const decryptedMasterKeyHashArrayBuffer = await this.encryptService.rsaDecrypt( new EncString(pubKeyEncryptedMasterKeyHash), privateKey, ); - - const masterKey = new SymmetricCryptoKey(decryptedMasterKeyArrayBuffer) as MasterKey; const masterKeyHash = Utils.fromBufferToUtf8(decryptedMasterKeyHashArrayBuffer); return { diff --git a/libs/common/src/admin-console/models/domain/encrypted-organization-key.ts b/libs/common/src/admin-console/models/domain/encrypted-organization-key.ts index 67a5b0dd123..984d80ba519 100644 --- a/libs/common/src/admin-console/models/domain/encrypted-organization-key.ts +++ b/libs/common/src/admin-console/models/domain/encrypted-organization-key.ts @@ -31,8 +31,10 @@ export class EncryptedOrganizationKey implements BaseEncryptedOrganizationKey { constructor(private key: string) {} async decrypt(encryptService: EncryptService, privateKey: UserPrivateKey) { - const decValue = await encryptService.rsaDecrypt(this.encryptedOrganizationKey, privateKey); - return new SymmetricCryptoKey(decValue) as OrgKey; + return (await encryptService.decapsulateKeyUnsigned( + this.encryptedOrganizationKey, + privateKey, + )) as OrgKey; } get encryptedOrganizationKey() { diff --git a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts index 8516400fe09..76c2d443d1d 100644 --- a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts +++ b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts @@ -100,7 +100,7 @@ describe("PasswordResetEnrollmentServiceImplementation", () => { activeAccountSubject.next(Object.assign(user1AccountInfo, { id: "userId" as UserId })); keyService.getUserKey.mockResolvedValue({ key: "key" } as any); - encryptService.rsaEncrypt.mockResolvedValue(encryptedKey as any); + encryptService.encapsulateKeyUnsigned.mockResolvedValue(encryptedKey as any); await service.enroll("orgId"); @@ -122,7 +122,7 @@ describe("PasswordResetEnrollmentServiceImplementation", () => { }; const encryptedKey = { encryptedString: "encryptedString" }; organizationApiService.getKeys.mockResolvedValue(orgKeyResponse as any); - encryptService.rsaEncrypt.mockResolvedValue(encryptedKey as any); + encryptService.encapsulateKeyUnsigned.mockResolvedValue(encryptedKey as any); await service.enroll("orgId", "userId", { key: "key" } as any); diff --git a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts index 824521d8a2e..ef9c5ad3265 100644 --- a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts +++ b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts @@ -51,7 +51,7 @@ export class PasswordResetEnrollmentServiceImplementation userId ?? (await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id)))); userKey = userKey ?? (await this.keyService.getUserKey(userId)); // RSA Encrypt user's userKey.key with organization public key - const encryptedKey = await this.encryptService.rsaEncrypt(userKey.key, orgPublicKey); + const encryptedKey = await this.encryptService.encapsulateKeyUnsigned(userKey, orgPublicKey); const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest(); resetRequest.resetPasswordKey = encryptedKey.encryptedString; diff --git a/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts b/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts index 91e10f19069..a1e54f7064f 100644 --- a/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts +++ b/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts @@ -35,7 +35,38 @@ export abstract class EncryptService { key: SymmetricCryptoKey, decryptTrace?: string, ): Promise; + + /** + * Encapsulates a symmetric key with an asymmetric public key + * Note: This does not establish sender authenticity + * @param sharedKey - The symmetric key that is to be shared + * @param encapsulationKey - The encapsulation key (public key) of the receiver that the key is shared with + */ + abstract encapsulateKeyUnsigned( + sharedKey: SymmetricCryptoKey, + encapsulationKey: Uint8Array, + ): Promise; + /** + * Decapsulates a shared symmetric key with an asymmetric private key + * Note: This does not establish sender authenticity + * @param encryptedSharedKey - The encrypted shared symmetric key + * @param decapsulationKey - The key to decapsulate with (private key) + */ + abstract decapsulateKeyUnsigned( + encryptedSharedKey: EncString, + decapsulationKey: Uint8Array, + ): Promise; + /** + * @deprecated Use encapsulateKeyUnsigned instead + * @param data - The data to encrypt + * @param publicKey - The public key to encrypt with + */ abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise; + /** + * @deprecated Use decapsulateKeyUnsigned instead + * @param data - The ciphertext to decrypt + * @param privateKey - The privateKey to decrypt with + */ abstract rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise; /** * @deprecated Replaced by BulkEncryptService, remove once the feature is tested and the featureflag PM-4154-multi-worker-encryption-service is removed diff --git a/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts index 10d29198ada..4b299c9c6e6 100644 --- a/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts @@ -235,42 +235,22 @@ export class EncryptServiceImplementation implements EncryptService { } } - async rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise { - if (data == null) { - throw new Error("No data provided for encryption."); + async encapsulateKeyUnsigned( + sharedKey: SymmetricCryptoKey, + encapsulationKey: Uint8Array, + ): Promise { + if (sharedKey == null) { + throw new Error("No sharedKey provided for encapsulation"); } - - if (publicKey == null) { - throw new Error("No public key provided for encryption."); - } - const encrypted = await this.cryptoFunctionService.rsaEncrypt(data, publicKey, "sha1"); - return new EncString(EncryptionType.Rsa2048_OaepSha1_B64, Utils.fromBufferToB64(encrypted)); + return await this.rsaEncrypt(sharedKey.toEncoded(), encapsulationKey); } - async rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise { - if (data == null) { - throw new Error("[Encrypt service] rsaDecrypt: No data provided for decryption."); - } - - let algorithm: "sha1" | "sha256"; - switch (data.encryptionType) { - case EncryptionType.Rsa2048_OaepSha1_B64: - case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64: - algorithm = "sha1"; - break; - case EncryptionType.Rsa2048_OaepSha256_B64: - case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64: - algorithm = "sha256"; - break; - default: - throw new Error("Invalid encryption type."); - } - - if (privateKey == null) { - throw new Error("[Encrypt service] rsaDecrypt: No private key provided for decryption."); - } - - return this.cryptoFunctionService.rsaDecrypt(data.dataBytes, privateKey, algorithm); + async decapsulateKeyUnsigned( + encryptedSharedKey: EncString, + decapsulationKey: Uint8Array, + ): Promise { + const keyBytes = await this.rsaDecrypt(encryptedSharedKey, decapsulationKey); + return new SymmetricCryptoKey(keyBytes); } /** @@ -341,4 +321,42 @@ export class EncryptServiceImplementation implements EncryptService { this.logDecryptError(msg, keyEncType, dataEncType, decryptContext); } } + + async rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise { + if (data == null) { + throw new Error("No data provided for encryption."); + } + + if (publicKey == null) { + throw new Error("No public key provided for encryption."); + } + const encrypted = await this.cryptoFunctionService.rsaEncrypt(data, publicKey, "sha1"); + return new EncString(EncryptionType.Rsa2048_OaepSha1_B64, Utils.fromBufferToB64(encrypted)); + } + + async rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise { + if (data == null) { + throw new Error("[Encrypt service] rsaDecrypt: No data provided for decryption."); + } + + let algorithm: "sha1" | "sha256"; + switch (data.encryptionType) { + case EncryptionType.Rsa2048_OaepSha1_B64: + case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64: + algorithm = "sha1"; + break; + case EncryptionType.Rsa2048_OaepSha256_B64: + case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64: + algorithm = "sha256"; + break; + default: + throw new Error("Invalid encryption type."); + } + + if (privateKey == null) { + throw new Error("[Encrypt service] rsaDecrypt: No private key provided for decryption."); + } + + return this.cryptoFunctionService.rsaDecrypt(data.dataBytes, privateKey, algorithm); + } } diff --git a/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts b/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts index c65c78d88d7..4cbe3a3da90 100644 --- a/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts @@ -412,7 +412,8 @@ describe("EncryptService", () => { }); describe("rsa", () => { - const data = makeStaticByteArray(10, 100); + const data = makeStaticByteArray(64, 100); + const testKey = new SymmetricCryptoKey(data); const encryptedData = makeStaticByteArray(10, 150); const publicKey = makeStaticByteArray(10, 200); const privateKey = makeStaticByteArray(10, 250); @@ -422,22 +423,26 @@ describe("EncryptService", () => { return new EncString(EncryptionType.Rsa2048_OaepSha1_B64, Utils.fromBufferToB64(data)); } - describe("rsaEncrypt", () => { + describe("encapsulateKeyUnsigned", () => { it("throws if no data is provided", () => { - return expect(encryptService.rsaEncrypt(null, publicKey)).rejects.toThrow("No data"); + return expect(encryptService.encapsulateKeyUnsigned(null, publicKey)).rejects.toThrow( + "No sharedKey provided for encapsulation", + ); }); it("throws if no public key is provided", () => { - return expect(encryptService.rsaEncrypt(data, null)).rejects.toThrow("No public key"); + return expect(encryptService.encapsulateKeyUnsigned(testKey, null)).rejects.toThrow( + "No public key", + ); }); it("encrypts data with provided key", async () => { cryptoFunctionService.rsaEncrypt.mockResolvedValue(encryptedData); - const actual = await encryptService.rsaEncrypt(data, publicKey); + const actual = await encryptService.encapsulateKeyUnsigned(testKey, publicKey); expect(cryptoFunctionService.rsaEncrypt).toBeCalledWith( - expect.toEqualBuffer(data), + expect.toEqualBuffer(testKey.key), expect.toEqualBuffer(publicKey), "sha1", ); @@ -447,13 +452,17 @@ describe("EncryptService", () => { }); }); - describe("rsaDecrypt", () => { + describe("decapsulateKeyUnsigned", () => { it("throws if no data is provided", () => { - return expect(encryptService.rsaDecrypt(null, privateKey)).rejects.toThrow("No data"); + return expect(encryptService.decapsulateKeyUnsigned(null, privateKey)).rejects.toThrow( + "No data", + ); }); it("throws if no private key is provided", () => { - return expect(encryptService.rsaDecrypt(encString, null)).rejects.toThrow("No private key"); + return expect(encryptService.decapsulateKeyUnsigned(encString, null)).rejects.toThrow( + "No private key", + ); }); it.each([EncryptionType.AesCbc256_B64, EncryptionType.AesCbc256_HmacSha256_B64])( @@ -461,16 +470,16 @@ describe("EncryptService", () => { async (encType) => { encString.encryptionType = encType; - await expect(encryptService.rsaDecrypt(encString, privateKey)).rejects.toThrow( - "Invalid encryption type", - ); + await expect( + encryptService.decapsulateKeyUnsigned(encString, privateKey), + ).rejects.toThrow("Invalid encryption type"); }, ); it("decrypts data with provided key", async () => { cryptoFunctionService.rsaDecrypt.mockResolvedValue(data); - const actual = await encryptService.rsaDecrypt(makeEncString(data), privateKey); + const actual = await encryptService.decapsulateKeyUnsigned(makeEncString(data), privateKey); expect(cryptoFunctionService.rsaDecrypt).toBeCalledWith( expect.toEqualBuffer(data), @@ -478,7 +487,7 @@ describe("EncryptService", () => { "sha1", ); - expect(actual).toEqualBuffer(data); + expect(actual.key).toEqualBuffer(data); }); }); }); diff --git a/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts b/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts index bddcf6185f0..a2211753f4e 100644 --- a/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts +++ b/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts @@ -161,7 +161,7 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { deviceKeyEncryptedDevicePrivateKey, ] = await Promise.all([ // Encrypt user key with the DevicePublicKey - this.encryptService.rsaEncrypt(userKey.key, devicePublicKey), + this.encryptService.encapsulateKeyUnsigned(userKey, devicePublicKey), // Encrypt devicePublicKey with user key this.encryptService.encrypt(devicePublicKey, userKey), @@ -285,8 +285,8 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { ); // Encrypt the brand new user key with the now-decrypted public key for the device - const encryptedNewUserKey = await this.encryptService.rsaEncrypt( - newUserKey.key, + const encryptedNewUserKey = await this.encryptService.encapsulateKeyUnsigned( + newUserKey, decryptedDevicePublicKey, ); @@ -401,12 +401,12 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { ); // Attempt to decrypt encryptedUserDataKey with devicePrivateKey - const userKey = await this.encryptService.rsaDecrypt( + const userKey = await this.encryptService.decapsulateKeyUnsigned( new EncString(encryptedUserKey.encryptedString), devicePrivateKey, ); - return new SymmetricCryptoKey(userKey) as UserKey; + return userKey as UserKey; // FIXME: Remove when updating file. Eslint update // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { diff --git a/libs/common/src/key-management/device-trust/services/device-trust.service.spec.ts b/libs/common/src/key-management/device-trust/services/device-trust.service.spec.ts index 61c90c5b5ee..8431fe4cc35 100644 --- a/libs/common/src/key-management/device-trust/services/device-trust.service.spec.ts +++ b/libs/common/src/key-management/device-trust/services/device-trust.service.spec.ts @@ -416,7 +416,7 @@ describe("deviceTrustService", () => { .mockResolvedValue(mockUserKey); cryptoSvcRsaEncryptSpy = jest - .spyOn(encryptService, "rsaEncrypt") + .spyOn(encryptService, "encapsulateKeyUnsigned") .mockResolvedValue(mockDevicePublicKeyEncryptedUserKey); encryptServiceEncryptSpy = jest @@ -449,8 +449,8 @@ describe("deviceTrustService", () => { expect(cryptoSvcRsaEncryptSpy).toHaveBeenCalledTimes(1); // RsaEncrypt must be called w/ a user key array buffer of 64 bytes - const userKeyKey: Uint8Array = cryptoSvcRsaEncryptSpy.mock.calls[0][0]; - expect(userKeyKey.byteLength).toBe(64); + const userKey = cryptoSvcRsaEncryptSpy.mock.calls[0][0]; + expect(userKey.key.byteLength).toBe(64); expect(encryptServiceEncryptSpy).toHaveBeenCalledTimes(2); @@ -610,7 +610,7 @@ describe("deviceTrustService", () => { mockUserId, mockEncryptedDevicePrivateKey, mockEncryptedUserKey, - mockDeviceKey, + null, ); expect(result).toBeNull(); @@ -621,8 +621,8 @@ describe("deviceTrustService", () => { .spyOn(encryptService, "decryptToBytes") .mockResolvedValue(new Uint8Array(userKeyBytesLength)); const rsaDecryptSpy = jest - .spyOn(encryptService, "rsaDecrypt") - .mockResolvedValue(new Uint8Array(userKeyBytesLength)); + .spyOn(encryptService, "decapsulateKeyUnsigned") + .mockResolvedValue(new SymmetricCryptoKey(new Uint8Array(userKeyBytesLength))); const result = await deviceTrustService.decryptUserKeyWithDeviceKey( mockUserId, @@ -863,9 +863,9 @@ describe("deviceTrustService", () => { }); // Mock the encryption of the new user key with the decrypted public key - encryptService.rsaEncrypt.mockImplementationOnce((data, publicKey) => { - expect(data.byteLength).toBe(64); // New key should also be 64 bytes - expect(new Uint8Array(data)[0]).toBe(FakeNewUserKeyMarker); // New key should have the first byte be '1'; + encryptService.encapsulateKeyUnsigned.mockImplementationOnce((data, publicKey) => { + expect(data.key.byteLength).toBe(64); // New key should also be 64 bytes + expect(new Uint8Array(data.key)[0]).toBe(FakeNewUserKeyMarker); // New key should have the first byte be '1'; expect(new Uint8Array(publicKey)[0]).toBe(FakeDecryptedPublicKeyMarker); return Promise.resolve(new EncString("4.ZW5jcnlwdGVkdXNlcg==")); diff --git a/libs/key-management/src/key.service.spec.ts b/libs/key-management/src/key.service.spec.ts index 049ff7411c3..90d049b7293 100644 --- a/libs/key-management/src/key.service.spec.ts +++ b/libs/key-management/src/key.service.spec.ts @@ -557,8 +557,8 @@ describe("keyService", () => { return Promise.resolve(fakePrivateKeyDecryption(encryptedPrivateKey, userKey)); }); - encryptService.rsaDecrypt.mockImplementation((data, privateKey) => { - return Promise.resolve(fakeOrgKeyDecryption(data, privateKey)); + encryptService.decapsulateKeyUnsigned.mockImplementation((data, privateKey) => { + return Promise.resolve(new SymmetricCryptoKey(fakeOrgKeyDecryption(data, privateKey))); }); } diff --git a/libs/key-management/src/key.service.ts b/libs/key-management/src/key.service.ts index e4f07911661..baf1b86e160 100644 --- a/libs/key-management/src/key.service.ts +++ b/libs/key-management/src/key.service.ts @@ -493,7 +493,7 @@ export class DefaultKeyService implements KeyServiceAbstraction { throw new Error("No public key found."); } - const encShareKey = await this.encryptService.rsaEncrypt(shareKey.key, publicKey); + const encShareKey = await this.encryptService.encapsulateKeyUnsigned(shareKey, publicKey); return [encShareKey, shareKey as T]; } @@ -968,11 +968,11 @@ export class DefaultKeyService implements KeyServiceAbstraction { return this.stateProvider.getUser(userId, USER_ENCRYPTED_PROVIDER_KEYS).state$.pipe( // Convert each value in the record to it's own decryption observable convertValues(async (_, value) => { - const decrypted = await this.encryptService.rsaDecrypt( + const decapsulatedKey = await this.encryptService.decapsulateKeyUnsigned( new EncString(value), userPrivateKey, ); - return new SymmetricCryptoKey(decrypted) as ProviderKey; + return decapsulatedKey as ProviderKey; }), // switchMap since there are no side effects switchMap((encryptedProviderKeys) => { From 94d94875475288de257b7a311ff0a1f577e8d1e6 Mon Sep 17 00:00:00 2001 From: Andy Pixley <3723676+pixman20@users.noreply.github.com> Date: Tue, 15 Apr 2025 12:31:08 -0400 Subject: [PATCH 21/47] [BRE-777] Fixing output to match what's in gh-actions (#14292) --- .github/workflows/deploy-web.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml index 9b890491282..1cde8dd636a 100644 --- a/.github/workflows/deploy-web.yml +++ b/.github/workflows/deploy-web.yml @@ -242,7 +242,7 @@ jobs: run: | # If run-id was used, get the commit from the download-latest-artifacts-run-id step if [ "${{ inputs.build-web-run-id }}" ]; then - echo "commit=${{ steps.download-latest-artifacts-run-id.outputs.artifact_build_commit }}" >> $GITHUB_OUTPUT + echo "commit=${{ steps.download-latest-artifacts-run-id.outputs.artifact-build-commit }}" >> $GITHUB_OUTPUT elif [ "${{ steps.download-latest-artifacts.outcome }}" == "failure" ]; then # If the download-latest-artifacts step failed, query the GH API to get the commit SHA of the artifact that was just built with trigger-build-web. @@ -251,7 +251,7 @@ jobs: else # Set the commit to the output of step download-latest-artifacts. - echo "commit=${{ steps.download-latest-artifacts.outputs.artifact_build_commit }}" >> $GITHUB_OUTPUT + echo "commit=${{ steps.download-latest-artifacts.outputs.artifact-build-commit }}" >> $GITHUB_OUTPUT fi notify-start: From f74d7e5fd5d4a4602cea2c6afc55bc594d46ce1e Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Tue, 15 Apr 2025 14:17:53 -0400 Subject: [PATCH 22/47] [PM-20239] Initializing `nx` (#14276) * Add .nx file to .gitignore Co-authored-by: Addison Beck * Add nx package Co-authored-by: Addison Beck * Add nx.json file Co-authored-by: Addison Beck * Add nx to Platform ownership --------- Co-authored-by: Addison Beck --- .github/renovate.json5 | 1 + .gitignore | 3 + nx.json | 10 + package-lock.json | 528 ++++++++++++++++++++++++++++++++++++++++- package.json | 1 + 5 files changed, 538 insertions(+), 5 deletions(-) create mode 100644 nx.json diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 973ba700349..c4202ed2a68 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -250,6 +250,7 @@ "napi-derive", "node-forge", "node-ipc", + "nx", "oo7", "oslog", "pin-project", diff --git a/.gitignore b/.gitignore index d0d8edd596c..e865fa6a8fb 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,6 @@ storybook-static # Local app configuration apps/**/config/local.json + +# Nx +.nx diff --git a/nx.json b/nx.json new file mode 100644 index 00000000000..7da50182873 --- /dev/null +++ b/nx.json @@ -0,0 +1,10 @@ +{ + "cacheDirectory": ".nx/cache", + "defaultBase": "main", + "namedInputs": { + "default": ["{projectRoot}/**/*"], + "production": ["!{projectRoot}/**/*.spec.ts"] + }, + "parallel": 4, + "targetDefaults": {} +} diff --git a/package-lock.json b/package-lock.json index 3cba61db6f8..cb9baf4fafe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -156,6 +156,7 @@ "json5": "2.2.3", "lint-staged": "15.4.1", "mini-css-extract-plugin": "2.9.2", + "nx": "20.8.0", "postcss": "8.5.1", "postcss-loader": "8.1.1", "prettier": "3.4.2", @@ -5954,6 +5955,34 @@ "node": "*" } }, + "node_modules/@emnapi/core": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.1.tgz", + "integrity": "sha512-4JFstCTaToCFrPqrGzgkF8N2NHjtsaY4uRh6brZQ5L9e4wbMieX8oDT8N7qfVFTQecHFEtkj4ve49VIZ3mKVqw==", + "dev": true, + "dependencies": { + "@emnapi/wasi-threads": "1.0.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.1.tgz", + "integrity": "sha512-LMshMVP0ZhACNjQNYXiU1iZJ6QCcv0lUdPDPugqGvCGXt5xtRVBPdtA0qU12pEXZzpWAhWlZYptfdAFq10DOVQ==", + "dev": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.1.tgz", + "integrity": "sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw==", + "dev": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emotion/babel-plugin": { "version": "11.13.5", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", @@ -8223,6 +8252,17 @@ "url": "https://github.com/sponsors/Brooooooklyn" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.4.tgz", + "integrity": "sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ==", + "dev": true, + "dependencies": { + "@emnapi/core": "^1.1.0", + "@emnapi/runtime": "^1.1.0", + "@tybys/wasm-util": "^0.9.0" + } + }, "node_modules/@ng-select/ng-select": { "version": "13.9.1", "resolved": "https://registry.npmjs.org/@ng-select/ng-select/-/ng-select-13.9.1.tgz", @@ -8758,6 +8798,166 @@ "node": "^16.13.0 || >=18.0.0" } }, + "node_modules/@nx/nx-darwin-arm64": { + "version": "20.8.0", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-20.8.0.tgz", + "integrity": "sha512-A6Te2KlINtcOo/depXJzPyjbk9E0cmgbom/sm/49XdQ8G94aDfyIIY1RIdwmDCK5NVd74KFG3JIByTk5+VnAhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-darwin-x64": { + "version": "20.8.0", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-20.8.0.tgz", + "integrity": "sha512-UpqayUjgalArXaDvOoshqSelTrEp42cGDsZGy0sqpxwBpm3oPQ8wE1d7oBAmRo208rAxOuFP0LZRFUqRrwGvLA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-freebsd-x64": { + "version": "20.8.0", + "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-20.8.0.tgz", + "integrity": "sha512-dUR2fsLyKZYMHByvjy2zvmdMbsdXAiP+6uTlIAuu8eHMZ2FPQCAtt7lPYLwOFUxUXChbek2AJ+uCI0gRAgK/eg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-arm-gnueabihf": { + "version": "20.8.0", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-20.8.0.tgz", + "integrity": "sha512-GuZ7t0SzSX5ksLYva7koKZovQ5h/Kr1pFbOsQcBf3VLREBqFPSz6t7CVYpsIsMhiu/I3EKq6FZI3wDOJbee5uw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-arm64-gnu": { + "version": "20.8.0", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-20.8.0.tgz", + "integrity": "sha512-CiI955Q+XZmBBZ7cQqQg0MhGEFwZIgSpJnjPfWBt3iOYP8aE6nZpNOkmD7O8XcN/nEwwyeCOF8euXqEStwsk8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-arm64-musl": { + "version": "20.8.0", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-20.8.0.tgz", + "integrity": "sha512-Iy9DpvVisxsfNh4gOinmMQ4cLWdBlgvt1wmry1UwvcXg479p1oJQ1Kp1wksUZoWYqrAG8VPZUmkE0f7gjyHTGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-x64-gnu": { + "version": "20.8.0", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-20.8.0.tgz", + "integrity": "sha512-kZrrXXzVSbqwmdTmQ9xL4Jhi0/FSLrePSxYCL9oOM3Rsj0lmo/aC9kz4NBv1ZzuqT7fumpBOnhqiL1QyhOWOeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-linux-x64-musl": { + "version": "20.8.0", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-20.8.0.tgz", + "integrity": "sha512-0l9jEMN8NhULKYCFiDF7QVpMMNG40duya+OF8dH0OzFj52N0zTsvsgLY72TIhslCB/cC74oAzsmWEIiFslscnA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-win32-arm64-msvc": { + "version": "20.8.0", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-20.8.0.tgz", + "integrity": "sha512-5miZJmRSwx1jybBsiB3NGocXL9TxGdT2D+dOqR2fsLklpGz0ItEWm8+i8lhDjgOdAr2nFcuQUfQMY57f9FOHrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nx/nx-win32-x64-msvc": { + "version": "20.8.0", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-20.8.0.tgz", + "integrity": "sha512-0P5r+bDuSNvoWys+6C1/KqGpYlqwSHpigCcyRzR62iZpT3OooZv+nWO06RlURkxMR8LNvYXTSSLvoLkjxqM8uQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@parcel/watcher": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", @@ -10856,6 +11056,15 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", + "integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==", + "dev": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz", @@ -13196,6 +13405,59 @@ "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", "license": "BSD-2-Clause" }, + "node_modules/@yarnpkg/parsers": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@yarnpkg/parsers/-/parsers-3.0.2.tgz", + "integrity": "sha512-/HcYgtUSiJiot/XWGLOlGxPYUG65+/31V8oqk17vZLW1xlCoR4PampyePljOxY2n8/3jz9+tIFzICsyGujJZoA==", + "dev": true, + "dependencies": { + "js-yaml": "^3.10.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/@yarnpkg/parsers/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@yarnpkg/parsers/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@yarnpkg/parsers/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/@zkochan/js-yaml": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@zkochan/js-yaml/-/js-yaml-0.0.7.tgz", + "integrity": "sha512-nrUSn7hzt7J6JWgWGz78ZYI8wj+gdIJdk0Ynjpp8l+trkn58Uqsf6RYrYkEK+3X18EX+TNdtJI0WxAtc+L84SQ==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/7zip-bin": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", @@ -14298,11 +14560,10 @@ } }, "node_modules/axios": { - "version": "1.7.9", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", - "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", "dev": true, - "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -18077,6 +18338,18 @@ "node": ">=10.13.0" } }, + "node_modules/enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -20259,6 +20532,43 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/front-matter": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/front-matter/-/front-matter-4.0.2.tgz", + "integrity": "sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg==", + "dev": true, + "dependencies": { + "js-yaml": "^3.13.1" + } + }, + "node_modules/front-matter/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/front-matter/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/front-matter/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -28045,6 +28355,12 @@ "license": "MIT", "peer": true }, + "node_modules/node-machine-id": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.12.tgz", + "integrity": "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==", + "dev": true + }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -28513,6 +28829,209 @@ "integrity": "sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==", "license": "MIT" }, + "node_modules/nx": { + "version": "20.8.0", + "resolved": "https://registry.npmjs.org/nx/-/nx-20.8.0.tgz", + "integrity": "sha512-+BN5B5DFBB5WswD8flDDTnr4/bf1VTySXOv60aUAllHqR+KS6deT0p70TTMZF4/A2n/L2UCWDaDro37MGaYozA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@napi-rs/wasm-runtime": "0.2.4", + "@yarnpkg/lockfile": "^1.1.0", + "@yarnpkg/parsers": "3.0.2", + "@zkochan/js-yaml": "0.0.7", + "axios": "^1.8.3", + "chalk": "^4.1.0", + "cli-cursor": "3.1.0", + "cli-spinners": "2.6.1", + "cliui": "^8.0.1", + "dotenv": "~16.4.5", + "dotenv-expand": "~11.0.6", + "enquirer": "~2.3.6", + "figures": "3.2.0", + "flat": "^5.0.2", + "front-matter": "^4.0.2", + "ignore": "^5.0.4", + "jest-diff": "^29.4.1", + "jsonc-parser": "3.2.0", + "lines-and-columns": "2.0.3", + "minimatch": "9.0.3", + "node-machine-id": "1.1.12", + "npm-run-path": "^4.0.1", + "open": "^8.4.0", + "ora": "5.3.0", + "resolve.exports": "2.0.3", + "semver": "^7.5.3", + "string-width": "^4.2.3", + "tar-stream": "~2.2.0", + "tmp": "~0.2.1", + "tsconfig-paths": "^4.1.2", + "tslib": "^2.3.0", + "yaml": "^2.6.0", + "yargs": "^17.6.2", + "yargs-parser": "21.1.1" + }, + "bin": { + "nx": "bin/nx.js", + "nx-cloud": "bin/nx-cloud.js" + }, + "optionalDependencies": { + "@nx/nx-darwin-arm64": "20.8.0", + "@nx/nx-darwin-x64": "20.8.0", + "@nx/nx-freebsd-x64": "20.8.0", + "@nx/nx-linux-arm-gnueabihf": "20.8.0", + "@nx/nx-linux-arm64-gnu": "20.8.0", + "@nx/nx-linux-arm64-musl": "20.8.0", + "@nx/nx-linux-x64-gnu": "20.8.0", + "@nx/nx-linux-x64-musl": "20.8.0", + "@nx/nx-win32-arm64-msvc": "20.8.0", + "@nx/nx-win32-x64-msvc": "20.8.0" + }, + "peerDependencies": { + "@swc-node/register": "^1.8.0", + "@swc/core": "^1.3.85" + }, + "peerDependenciesMeta": { + "@swc-node/register": { + "optional": true + }, + "@swc/core": { + "optional": true + } + } + }, + "node_modules/nx/node_modules/cli-spinners": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", + "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nx/node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/nx/node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/nx/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/nx/node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, + "node_modules/nx/node_modules/lines-and-columns": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.3.tgz", + "integrity": "sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/nx/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/nx/node_modules/ora": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.3.0.tgz", + "integrity": "sha512-zAKMgGXUim0Jyd6CXK9lraBnD3H5yPGBPPOkC23a2BG6hsm4Zu6OQSjQuEtV0BHDf4aKHcUFvJiGRrFuW3MG8g==", + "dev": true, + "dependencies": { + "bl": "^4.0.3", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "log-symbols": "^4.0.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nx/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/nx/node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/nx/node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -31294,7 +31813,6 @@ "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" } diff --git a/package.json b/package.json index f24588c4c82..28d243e1c32 100644 --- a/package.json +++ b/package.json @@ -118,6 +118,7 @@ "json5": "2.2.3", "lint-staged": "15.4.1", "mini-css-extract-plugin": "2.9.2", + "nx": "20.8.0", "postcss": "8.5.1", "postcss-loader": "8.1.1", "prettier": "3.4.2", From e3d1ef456e8dfaba8fbdac1a420f513a8acebd17 Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Tue, 15 Apr 2025 14:37:12 -0400 Subject: [PATCH 23/47] [PM-14909] Add data/state for security task completion notification (#14279) * include tasks with notification cipher data * send security task information with update success message for notification * mark completed cipher updates with tasks as complete * refactor notification confirmation components and add stories * add keyhole icon * add conditional footer button to notification confirmation component * add external link icon * add external link icon to action button * add notification confirmation footer story * use keyhole icon if there are no additional security tasks to complete * add new message catalog entries to chrome.i18n * reimplement sending security task information with update success message for notification * open tasks in extension from confirmation notification button * update vault message key and dismiss all security tasks for a given cipher upon password update * resolve changes against updated main branch basis * put task fetching behind feature flag and update tests * cleanup * more cleanup --- .../abstractions/notification.background.ts | 1 + .../notification.background.spec.ts | 98 +++++++++++++- .../background/notification.background.ts | 123 ++++++++++++++++-- .../abstractions/notification-bar.ts | 7 +- apps/browser/src/autofill/notification/bar.ts | 6 +- ...rlay-notifications-content.service.spec.ts | 24 ++-- .../overlay-notifications-content.service.ts | 6 +- .../browser/src/background/main.background.ts | 23 ++-- 8 files changed, 247 insertions(+), 41 deletions(-) diff --git a/apps/browser/src/autofill/background/abstractions/notification.background.ts b/apps/browser/src/autofill/background/abstractions/notification.background.ts index 851f07576dd..6b3c91a109c 100644 --- a/apps/browser/src/autofill/background/abstractions/notification.background.ts +++ b/apps/browser/src/autofill/background/abstractions/notification.background.ts @@ -95,6 +95,7 @@ type NotificationBackgroundExtensionMessageHandlers = { unlockCompleted: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgGetFolderData: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgCloseNotificationBar: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; + bgOpenAtRisksPasswords: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgAdjustNotificationBar: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgAddLogin: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgChangedPassword: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index ebdd244e140..ffc416ab62a 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -1,5 +1,5 @@ import { mock, MockProxy } from "jest-mock-extended"; -import { BehaviorSubject, firstValueFrom } from "rxjs"; +import { BehaviorSubject, firstValueFrom, of } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { DefaultPolicyService } from "@bitwarden/common/admin-console/services/policy/default-policy.service"; @@ -12,6 +12,7 @@ import { UserNotificationSettingsService } from "@bitwarden/common/autofill/serv import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { SelfHostedEnvironment } from "@bitwarden/common/platform/services/default-environment.service"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { UserId } from "@bitwarden/common/types/guid"; @@ -19,6 +20,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service"; +import { TaskService, SecurityTask } from "@bitwarden/common/vault/tasks"; import { BrowserApi } from "../../platform/browser/browser-api"; import { NotificationQueueMessageType } from "../enums/notification-queue-message-type.enum"; @@ -46,6 +48,8 @@ jest.mock("rxjs", () => { }); describe("NotificationBackground", () => { + const messagingService = mock(); + const taskService = mock(); let notificationBackground: NotificationBackground; const autofillService = mock(); const cipherService = mock(); @@ -88,6 +92,8 @@ describe("NotificationBackground", () => { policyService, themeStateService, userNotificationSettingsService, + taskService, + messagingService, ); }); @@ -201,8 +207,8 @@ describe("NotificationBackground", () => { await flushPromises(); expect(notificationBackground["handleSaveCipherMessage"]).toHaveBeenCalledWith( - message.data.commandToRetry.message, - message.data.commandToRetry.sender, + message.data?.commandToRetry?.message, + message.data?.commandToRetry?.sender, ); }); }); @@ -498,7 +504,7 @@ describe("NotificationBackground", () => { expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith( null, "example.com", - message.data.newPassword, + message.data?.newPassword, sender.tab, true, ); @@ -570,7 +576,7 @@ describe("NotificationBackground", () => { expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith( "cipher-id", "example.com", - message.data.newPassword, + message.data?.newPassword, sender.tab, ); }); @@ -618,7 +624,7 @@ describe("NotificationBackground", () => { expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith( "cipher-id", "example.com", - message.data.newPassword, + message.data?.newPassword, sender.tab, ); }); @@ -844,6 +850,86 @@ describe("NotificationBackground", () => { ); }); + it("completes password update notification with a security task notice if any are present for the cipher, and dismisses tasks for the updated cipher", async () => { + const mockCipherId = "testId"; + const mockOrgId = "testOrgId"; + const mockSecurityTask = { + id: "testTaskId", + organizationId: mockOrgId, + cipherId: mockCipherId, + type: 0, + status: 0, + creationDate: new Date(), + revisionDate: new Date(), + } as SecurityTask; + const mockSecurityTask2 = { + ...mockSecurityTask, + id: "testTaskId2", + cipherId: "testId2", + } as SecurityTask; + taskService.tasksEnabled$.mockImplementation(() => of(true)); + taskService.pendingTasks$.mockImplementation(() => + of([mockSecurityTask, mockSecurityTask2]), + ); + jest + .spyOn(notificationBackground as any, "getNotificationFlag") + .mockResolvedValueOnce(true); + jest.spyOn(notificationBackground as any, "getOrgData").mockResolvedValueOnce([ + { + id: mockOrgId, + name: "Org Name, LLC", + productTierType: 3, + }, + ]); + + const tab = createChromeTabMock({ id: 1, url: "https://example.com" }); + const sender = mock({ tab }); + const message: NotificationBackgroundExtensionMessage = { + command: "bgSaveCipher", + edit: false, + folder: "folder-id", + }; + const queueMessage = mock({ + type: NotificationQueueMessageType.ChangePassword, + tab, + domain: "example.com", + newPassword: "newPassword", + }); + notificationBackground["notificationQueue"] = [queueMessage]; + const cipherView = mock({ + id: mockCipherId, + organizationId: mockOrgId, + login: { username: "testUser" }, + }); + getDecryptedCipherByIdSpy.mockResolvedValueOnce(cipherView); + + sendMockExtensionMessage(message, sender); + await flushPromises(); + + expect(editItemSpy).not.toHaveBeenCalled(); + expect(createWithServerSpy).not.toHaveBeenCalled(); + expect(updatePasswordSpy).toHaveBeenCalledWith( + cipherView, + queueMessage.newPassword, + message.edit, + sender.tab, + mockCipherId, + ); + expect(updateWithServerSpy).toHaveBeenCalled(); + expect(tabSendMessageDataSpy).toHaveBeenCalledWith( + sender.tab, + "saveCipherAttemptCompleted", + { + cipherId: "testId", + task: { + orgName: "Org Name, LLC", + remainingTasksCount: 1, + }, + username: "testUser", + }, + ); + }); + it("updates the cipher password if the queue message was locked and an existing cipher has the same username as the message", async () => { const tab = createChromeTabMock({ id: 1, url: "https://example.com" }); const sender = mock({ tab }); diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index c2e90460dfc..1f0cc469e2c 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom, switchMap } from "rxjs"; +import { firstValueFrom, switchMap, map } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; @@ -22,16 +22,21 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; +import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum"; import { buildCipherIcon } from "@bitwarden/common/vault/icon/build-cipher-icon"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; +import { TaskService } from "@bitwarden/common/vault/tasks"; +import { SecurityTaskType } from "@bitwarden/common/vault/tasks/enums"; +import { SecurityTask } from "@bitwarden/common/vault/tasks/models/security-task"; import { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window"; import { BrowserApi } from "../../platform/browser/browser-api"; @@ -70,6 +75,8 @@ export default class NotificationBackground { bgChangedPassword: ({ message, sender }) => this.changedPassword(message, sender), bgCloseNotificationBar: ({ message, sender }) => this.handleCloseNotificationBarMessage(message, sender), + bgOpenAtRisksPasswords: ({ message, sender }) => + this.handleOpenAtRisksPasswordsMessage(message, sender), bgGetActiveUserServerConfig: () => this.getActiveUserServerConfig(), bgGetDecryptedCiphers: () => this.getNotificationCipherData(), bgGetEnableChangedPasswordPrompt: () => this.getEnableChangedPasswordPrompt(), @@ -106,6 +113,8 @@ export default class NotificationBackground { private policyService: PolicyService, private themeStateService: ThemeStateService, private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction, + private taskService: TaskService, + protected messagingService: MessagingService, ) {} init() { @@ -154,17 +163,20 @@ export default class NotificationBackground { firstValueFrom(this.domainSettingsService.showFavicons$), firstValueFrom(this.environmentService.environment$), ]); + const iconsServerUrl = env.getIconsUrl(); const activeUserId = await firstValueFrom( this.accountService.activeAccount$.pipe(getOptionalUserId), ); + const decryptedCiphers = await this.cipherService.getAllDecryptedForUrl( - currentTab.url, + currentTab?.url, activeUserId, ); return decryptedCiphers.map((view) => { const { id, name, reprompt, favorite, login } = view; + return { id, name, @@ -599,13 +611,13 @@ export default class NotificationBackground { try { await this.cipherService.createWithServer(cipher); await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", { - username: String(queueMessage?.username), - cipherId: String(cipher?.id), + username: queueMessage?.username && String(queueMessage.username), + cipherId: cipher?.id && String(cipher.id), }); await BrowserApi.tabSendMessage(tab, { command: "addedCipher" }); } catch (error) { await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", { - error: String(error.message), + error: error?.message && String(error.message), }); } } @@ -638,15 +650,49 @@ export default class NotificationBackground { return; } const cipher = await this.cipherService.encrypt(cipherView, userId); + + const shouldGetTasks = await this.getNotificationFlag(); + try { + const tasks = shouldGetTasks ? await this.getSecurityTasks(userId) : []; + const updatedCipherTask = tasks.find((task) => task.cipherId === cipherView?.id); + const cipherHasTask = !!updatedCipherTask?.id; + + let taskOrgName: string; + if (cipherHasTask && updatedCipherTask?.organizationId) { + const userOrgs = await this.getOrgData(); + taskOrgName = userOrgs.find(({ id }) => id === updatedCipherTask.organizationId)?.name; + } + + const taskData = cipherHasTask + ? { + remainingTasksCount: tasks.length - 1, + orgName: taskOrgName, + } + : undefined; + await this.cipherService.updateWithServer(cipher); + await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", { - username: String(cipherView?.login?.username), - cipherId: String(cipherView?.id), + username: cipherView?.login?.username && String(cipherView.login.username), + cipherId: cipherView?.id && String(cipherView.id), + task: taskData, }); + + // If the cipher had a security task, mark it as complete + if (cipherHasTask) { + // guard against multiple (redundant) security tasks per cipher + await Promise.all( + tasks.map((task) => { + if (task.cipherId === cipherView?.id) { + return this.taskService.markAsComplete(task.id, userId); + } + }), + ); + } } catch (error) { await BrowserApi.tabSendMessageData(tab, "saveCipherAttemptCompleted", { - error: String(error?.message), + error: error?.message && String(error.message), }); } } @@ -699,6 +745,32 @@ export default class NotificationBackground { return null; } + private async getSecurityTasks(userId: UserId) { + let tasks: SecurityTask[] = []; + + if (userId) { + tasks = await firstValueFrom( + this.taskService.tasksEnabled$(userId).pipe( + switchMap((tasksEnabled) => { + if (!tasksEnabled) { + return []; + } + + return this.taskService + .pendingTasks$(userId) + .pipe( + map((tasks) => + tasks.filter(({ type }) => type === SecurityTaskType.UpdateAtRiskCredential), + ), + ); + }), + ), + ); + } + + return tasks; + } + /** * Saves the current tab's domain to the never save list. * @@ -819,6 +891,41 @@ export default class NotificationBackground { }); } + /** + * Sends a message to the background to open the + * at-risk passwords extension view. Triggers + * notification closure as a side-effect. + * + * @param message - The extension message + * @param sender - The contextual sender of the message + */ + private async handleOpenAtRisksPasswordsMessage( + message: NotificationBackgroundExtensionMessage, + sender: chrome.runtime.MessageSender, + ) { + const browserAction = BrowserApi.getBrowserAction(); + + try { + // Set route of the popup before attempting to open it. + // If the vault is locked, this won't have an effect as the auth guards will + // redirect the user to the login page. + await browserAction.setPopup({ popup: "popup/index.html#/at-risk-passwords" }); + + await Promise.all([ + this.messagingService.send(VaultMessages.OpenAtRiskPasswords), + BrowserApi.tabSendMessageData(sender.tab, "closeNotificationBar", { + fadeOutNotification: !!message.fadeOutNotification, + }), + ]); + } finally { + // Reset the popup route to the default route so any subsequent + // popup openings will not open to the at-risk-passwords page. + await browserAction.setPopup({ + popup: "popup/index.html#/", + }); + } + } + /** * Sends a message back to the sender tab which triggers * an CSS adjustment of the notification bar. diff --git a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts index 7e2fdab04d3..cbfeffcf2f4 100644 --- a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts +++ b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts @@ -29,11 +29,14 @@ type NotificationBarIframeInitData = { }; type NotificationBarWindowMessage = { - cipherId?: string; command: string; + data?: { + cipherId?: string; + task?: NotificationTaskInfo; + username?: string; + }; error?: string; initData?: NotificationBarIframeInitData; - username?: string; }; type NotificationBarWindowMessageHandlers = { diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts index f544e75527c..d660790ee63 100644 --- a/apps/browser/src/autofill/notification/bar.ts +++ b/apps/browser/src/autofill/notification/bar.ts @@ -356,7 +356,8 @@ function openViewVaultItemPopout(e: Event, cipherId: string) { function handleSaveCipherConfirmation(message: NotificationBarWindowMessage) { const { theme, type } = notificationBarIframeInitData; - const { error, username, cipherId } = message; + const { error, data } = message; + const { username, cipherId, task } = data || {}; const i18n = getI18n(); const resolvedTheme = getResolvedTheme(theme ?? ThemeTypes.Light); @@ -371,8 +372,9 @@ function handleSaveCipherConfirmation(message: NotificationBarWindowMessage) { i18n, error, username: username ?? i18n.typeLogin, + task, handleOpenVault: (e) => cipherId && openViewVaultItemPopout(e, cipherId), - handleOpenTasks: () => {}, + handleOpenTasks: () => sendPlatformMessage({ command: "bgOpenAtRisksPasswords" }), }), document.body, ); diff --git a/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.spec.ts b/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.spec.ts index 8a8ccdf363b..28db10b35fa 100644 --- a/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.spec.ts +++ b/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.spec.ts @@ -23,8 +23,8 @@ describe("OverlayNotificationsContentService", () => { autofillInit = new AutofillInit( domQueryService, domElementVisibilityService, - null, - null, + undefined, + undefined, overlayNotificationsContentService, ); autofillInit.init(); @@ -89,7 +89,7 @@ describe("OverlayNotificationsContentService", () => { await flushPromises(); expect( - overlayNotificationsContentService["notificationBarIframeElement"].style.transform, + overlayNotificationsContentService["notificationBarIframeElement"]?.style.transform, ).toBe("translateX(100%)"); }); @@ -103,12 +103,12 @@ describe("OverlayNotificationsContentService", () => { }); await flushPromises(); - overlayNotificationsContentService["notificationBarIframeElement"].dispatchEvent( + overlayNotificationsContentService["notificationBarIframeElement"]?.dispatchEvent( new Event("load"), ); expect( - overlayNotificationsContentService["notificationBarIframeElement"].style.transform, + overlayNotificationsContentService["notificationBarIframeElement"]?.style.transform, ).toBe("translateX(0)"); }); @@ -134,7 +134,7 @@ describe("OverlayNotificationsContentService", () => { globalThis.dispatchEvent( new MessageEvent("message", { data: { command: "initNotificationBar" }, - source: overlayNotificationsContentService["notificationBarIframeElement"].contentWindow, + source: overlayNotificationsContentService["notificationBarIframeElement"]?.contentWindow, }), ); await flushPromises(); @@ -168,9 +168,9 @@ describe("OverlayNotificationsContentService", () => { data: { fadeOutNotification: true }, }); - expect(overlayNotificationsContentService["notificationBarIframeElement"].style.opacity).toBe( - "0", - ); + expect( + overlayNotificationsContentService["notificationBarIframeElement"]?.style.opacity, + ).toBe("0"); jest.advanceTimersByTime(150); @@ -210,7 +210,7 @@ describe("OverlayNotificationsContentService", () => { data: { height: 1000 }, }); - expect(overlayNotificationsContentService["notificationBarElement"].style.height).toBe( + expect(overlayNotificationsContentService["notificationBarElement"]?.style.height).toBe( "1000px", ); }); @@ -236,13 +236,13 @@ describe("OverlayNotificationsContentService", () => { sendMockExtensionMessage({ command: "saveCipherAttemptCompleted", - data: { error: "" }, + data: { error: undefined }, }); expect( overlayNotificationsContentService["notificationBarIframeElement"].contentWindow .postMessage, - ).toHaveBeenCalledWith({ command: "saveCipherAttemptCompleted", error: "" }, "*"); + ).toHaveBeenCalledWith({ command: "saveCipherAttemptCompleted", error: undefined }, "*"); }); }); diff --git a/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.ts b/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.ts index 662ec624dc4..519521feaa9 100644 --- a/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.ts +++ b/apps/browser/src/autofill/overlay/notifications/content/overlay-notifications-content.service.ts @@ -135,9 +135,13 @@ export class OverlayNotificationsContentService * @private */ private handleSaveCipherAttemptCompletedMessage(message: NotificationsExtensionMessage) { + // destructure error out of data + const { error, ...otherData } = message?.data || {}; + this.sendMessageToNotificationBarIframe({ command: "saveCipherAttemptCompleted", - error: message.data?.error, + data: Object.keys(otherData).length ? otherData : undefined, + error, }); } diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index a5001e0c5b7..c437698f525 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1186,6 +1186,17 @@ export default class MainBackground { this.authService, () => this.generatePasswordToClipboard(), ); + + this.taskService = new DefaultTaskService( + this.stateProvider, + this.apiService, + this.organizationService, + this.configService, + this.authService, + this.notificationsService, + messageListener, + ); + this.notificationBackground = new NotificationBackground( this.accountService, this.authService, @@ -1200,6 +1211,8 @@ export default class MainBackground { this.policyService, this.themeStateService, this.userNotificationSettingsService, + this.taskService, + this.messagingService, ); this.overlayNotificationsBackground = new OverlayNotificationsBackground( @@ -1304,16 +1317,6 @@ export default class MainBackground { this.configService, ); - this.taskService = new DefaultTaskService( - this.stateProvider, - this.apiService, - this.organizationService, - this.configService, - this.authService, - this.notificationsService, - messageListener, - ); - this.inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); this.ipcContentScriptManagerService = new IpcContentScriptManagerService(this.configService); From 4cddc40828920914776a05fc906b087ac3bfcf12 Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Tue, 15 Apr 2025 14:39:48 -0400 Subject: [PATCH 24/47] remove inlineAutofillMenuRefreshAddEditCipher message (#13805) --- .../src/autofill/background/overlay.background.spec.ts | 5 ----- apps/browser/src/autofill/background/overlay.background.ts | 1 - .../background/overlay.background.deprecated.spec.ts | 3 --- .../deprecated/background/overlay.background.deprecated.ts | 1 - 4 files changed, 10 deletions(-) diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts index 3085dbc2f8d..0fe4a459048 100644 --- a/apps/browser/src/autofill/background/overlay.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay.background.spec.ts @@ -117,7 +117,6 @@ describe("OverlayBackground", () => { let getFrameDetailsSpy: jest.SpyInstance; let tabsSendMessageSpy: jest.SpyInstance; let tabSendMessageDataSpy: jest.SpyInstance; - let sendMessageSpy: jest.SpyInstance; let getTabFromCurrentWindowIdSpy: jest.SpyInstance; let getTabSpy: jest.SpyInstance; let openUnlockPopoutSpy: jest.SpyInstance; @@ -228,7 +227,6 @@ describe("OverlayBackground", () => { tabSendMessageDataSpy = jest .spyOn(BrowserApi, "tabSendMessageData") .mockImplementation(() => Promise.resolve()); - sendMessageSpy = jest.spyOn(BrowserApi, "sendMessage"); getTabFromCurrentWindowIdSpy = jest.spyOn(BrowserApi, "getTabFromCurrentWindowId"); getTabSpy = jest.spyOn(BrowserApi, "getTab"); openUnlockPopoutSpy = jest.spyOn(overlayBackground as any, "openUnlockPopout"); @@ -1553,7 +1551,6 @@ describe("OverlayBackground", () => { await flushPromises(); expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled(); - expect(sendMessageSpy).toHaveBeenCalledWith("inlineAutofillMenuRefreshAddEditCipher"); expect(openAddEditVaultItemPopoutSpy).toHaveBeenCalled(); }); @@ -1579,7 +1576,6 @@ describe("OverlayBackground", () => { await flushPromises(); expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled(); - expect(sendMessageSpy).toHaveBeenCalledWith("inlineAutofillMenuRefreshAddEditCipher"); expect(openAddEditVaultItemPopoutSpy).toHaveBeenCalled(); }); @@ -1618,7 +1614,6 @@ describe("OverlayBackground", () => { await flushPromises(); expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled(); - expect(sendMessageSpy).toHaveBeenCalledWith("inlineAutofillMenuRefreshAddEditCipher"); expect(openAddEditVaultItemPopoutSpy).toHaveBeenCalled(); }); diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index fa0ae9b9b3e..a2088f50a11 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -2434,7 +2434,6 @@ export class OverlayBackground implements OverlayBackgroundInterface { cipherId: cipherView.id, cipherType: addNewCipherType, }); - await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher"); } catch (error) { this.logService.error("Error building cipher and opening add/edit vault item popout", error); } diff --git a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts index 128dd189878..68f8032350e 100644 --- a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts +++ b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts @@ -647,9 +647,6 @@ describe("OverlayBackground", () => { await flushPromises(); expect(overlayBackground["cipherService"].setAddEditCipherInfo).toHaveBeenCalled(); - expect(BrowserApi.sendMessage).toHaveBeenCalledWith( - "inlineAutofillMenuRefreshAddEditCipher", - ); expect(overlayBackground["openAddEditVaultItemPopout"]).toHaveBeenCalled(); }); }); diff --git a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts index d0fad4cd00e..c9eb442d75d 100644 --- a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts +++ b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts @@ -678,7 +678,6 @@ class LegacyOverlayBackground implements OverlayBackgroundInterface { ); await this.openAddEditVaultItemPopout(sender.tab, { cipherId: cipherView.id }); - await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher"); } /** From 8258ea39b0b33eaabf8320cea98b64208b96f931 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Tue, 15 Apr 2025 12:17:41 -0700 Subject: [PATCH 25/47] [PM-18903] Desktop sync issues (#13681) * [PM-18707] Use different BroadcasterSubscriptionId in base view component to avoid collision with desktop view component * [PM-18707] Use userId instead of payloadUserId for cipher notification syncs * [PM-19032] Live Sync on Desktop (#13851) * migrate the vault-items to an observables rather than async/promises - this helps keep data in sync with the service state and avoids race conditions * migrate the view component to an observables rather than async/promises - this helps keep data in sync with the service state and avoids race conditions * decrypt saved cipher from server * bump timeout for upserting ciphers * mark `go` as async in desktop vault - previously it was a floating promise * Revert "mark `go` as async in desktop vault" This reverts commit fd28f40b187c39fb30d1d1ab2972d398b2673419. * Revert "bump timeout for upserting ciphers" This reverts commit e963acc377b0018fb1f90d4e9d181959820e00b3. * move vault utilities to `common` rather than `lib` to avoid circular dependencies * use `perUserCache$` for `cipherViews$` to avoid new subscriptions from being created * use userId from observable rather than locally set to be the most up to date * [PM-18707] Add clearBuffer$ input to perUserCache$ helper so that the internal share replay buffers can be cleared * [PM-18707] Rework forceCipherViews$ to clearBuffer$ refactor - Add dependency for cipherDecryptionKeys$ for the cipherViews so that decryption is never attempted without keys * [PM-18707] Add overload to perUserCache to satisfy type checker * [PM-18707] Fix overloads * [PM-18707] Add check for empty failed to decrypt ciphers * [PM-18707] Mark vault component for check after observable emits. The cipherViews$ observable now persists between subscriptions, meaning that updates via the sync push notifications can occur outside the AngularZone causing delays in updating the view. --------- Co-authored-by: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Co-authored-by: Nick Krantz --- .../vault/app/vault/vault-items.component.ts | 3 - .../src/vault/app/vault/view.component.ts | 4 +- .../vault/individual-vault/vault.component.ts | 14 +- .../vault/components/add-edit.component.ts | 9 +- .../vault/components/vault-items.component.ts | 111 +++++++----- .../src/vault/components/view.component.ts | 167 +++++++++++------- .../internal/default-notifications.service.ts | 4 +- .../src/vault/services/cipher.service.ts | 64 +++---- .../tasks/services/default-task.service.ts | 5 +- .../src/vault/utils/observable-utilities.ts | 26 ++- 10 files changed, 239 insertions(+), 168 deletions(-) diff --git a/apps/desktop/src/vault/app/vault/vault-items.component.ts b/apps/desktop/src/vault/app/vault/vault-items.component.ts index b7a45bd2467..d5838459ff7 100644 --- a/apps/desktop/src/vault/app/vault/vault-items.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-items.component.ts @@ -27,9 +27,6 @@ export class VaultItemsComponent extends BaseVaultItemsComponent { // eslint-disable-next-line rxjs-angular/prefer-takeuntil searchBarService.searchText$.pipe(distinctUntilChanged()).subscribe((searchText) => { this.searchText = searchText; - // 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.search(200); }); } diff --git a/apps/desktop/src/vault/app/vault/view.component.ts b/apps/desktop/src/vault/app/vault/view.component.ts index 9ddf18fff93..e5f677cbca6 100644 --- a/apps/desktop/src/vault/app/vault/view.component.ts +++ b/apps/desktop/src/vault/app/vault/view.component.ts @@ -126,9 +126,7 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro } async ngOnChanges() { - await super.load(); - - if (this.cipher.decryptionFailure) { + if (this.cipher?.decryptionFailure) { DecryptionFailureDialogComponent.open(this.dialogService, { cipherIds: [this.cipherId as CipherId], }); diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index c2173e29ee0..7055f164a53 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -13,6 +13,7 @@ import { Subject, } from "rxjs"; import { + catchError, concatMap, debounceTime, filter, @@ -23,7 +24,6 @@ import { take, takeUntil, tap, - catchError, } from "rxjs/operators"; import { @@ -64,6 +64,7 @@ import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-repromp import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { ServiceUtils } from "@bitwarden/common/vault/service-utils"; +import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities"; import { DialogRef, DialogService, Icons, ToastService } from "@bitwarden/components"; import { AddEditFolderDialogComponent, @@ -138,7 +139,6 @@ const SearchTextDebounceInterval = 200; VaultFilterModule, VaultItemsModule, SharedModule, - DecryptionFailureDialogComponent, ], providers: [ RoutedVaultFilterService, @@ -348,9 +348,8 @@ export class VaultComponent implements OnInit, OnDestroy { ]).pipe( filter(([ciphers, filter]) => ciphers != undefined && filter != undefined), concatMap(async ([ciphers, filter, searchText]) => { - const failedCiphers = await firstValueFrom( - this.cipherService.failedToDecryptCiphers$(activeUserId), - ); + const failedCiphers = + (await firstValueFrom(this.cipherService.failedToDecryptCiphers$(activeUserId))) ?? []; const filterFunction = createFilterFunction(filter); // Append any failed to decrypt ciphers to the top of the cipher list const allCiphers = [...failedCiphers, ...ciphers]; @@ -472,6 +471,7 @@ export class VaultComponent implements OnInit, OnDestroy { firstSetup$ .pipe( switchMap(() => this.cipherService.failedToDecryptCiphers$(activeUserId)), + filterOutNullish(), map((ciphers) => ciphers.filter((c) => !c.isDeleted)), filter((ciphers) => ciphers.length > 0), take(1), @@ -528,6 +528,10 @@ export class VaultComponent implements OnInit, OnDestroy { this.isEmpty = collections?.length === 0 && ciphers?.length === 0; this.performingInitialLoad = false; this.refreshing = false; + + // Explicitly mark for check to ensure the view is updated + // Some sources are not always emitted within the Angular zone (e.g. ciphers updated via WS notifications) + this.changeDetectorRef.markForCheck(); }, ); } diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 2393863bb5f..b9defa8383d 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -422,10 +422,15 @@ export class AddEditComponent implements OnInit, OnDestroy { const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const cipher = await this.encryptCipher(activeUserId); + try { this.formPromise = this.saveCipher(cipher); - await this.formPromise; - this.cipher.id = cipher.id; + const savedCipher = await this.formPromise; + + // Reset local cipher from the saved cipher returned from the server + this.cipher = await savedCipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(savedCipher, activeUserId), + ); this.toastService.showToast({ variant: "success", title: null, diff --git a/libs/angular/src/vault/components/vault-items.component.ts b/libs/angular/src/vault/components/vault-items.component.ts index f7280cb74b3..852302cc0c4 100644 --- a/libs/angular/src/vault/components/vault-items.component.ts +++ b/libs/angular/src/vault/components/vault-items.component.ts @@ -1,13 +1,22 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; -import { BehaviorSubject, Subject, firstValueFrom, from, switchMap, takeUntil } from "rxjs"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { + BehaviorSubject, + Subject, + combineLatest, + filter, + from, + of, + switchMap, + takeUntil, +} from "rxjs"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -21,17 +30,17 @@ export class VaultItemsComponent implements OnInit, OnDestroy { loaded = false; ciphers: CipherView[] = []; - filter: (cipher: CipherView) => boolean = null; deleted = false; organization: Organization; protected searchPending = false; - private userId: UserId; + /** Construct filters as an observable so it can be appended to the cipher stream. */ + private _filter$ = new BehaviorSubject<(cipher: CipherView) => boolean | null>(null); private destroy$ = new Subject(); - private searchTimeout: any = null; private isSearchable: boolean = false; private _searchText$ = new BehaviorSubject(""); + get searchText() { return this._searchText$.value; } @@ -39,18 +48,28 @@ export class VaultItemsComponent implements OnInit, OnDestroy { this._searchText$.next(value); } + get filter() { + return this._filter$.value; + } + + set filter(value: (cipher: CipherView) => boolean | null) { + this._filter$.next(value); + } + constructor( protected searchService: SearchService, protected cipherService: CipherService, protected accountService: AccountService, - ) {} + ) { + this.subscribeToCiphers(); + } async ngOnInit() { - this.userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); - - this._searchText$ + combineLatest([getUserId(this.accountService.activeAccount$), this._searchText$]) .pipe( - switchMap((searchText) => from(this.searchService.isSearchable(this.userId, searchText))), + switchMap(([userId, searchText]) => + from(this.searchService.isSearchable(userId, searchText)), + ), takeUntil(this.destroy$), ) .subscribe((isSearchable) => { @@ -80,23 +99,6 @@ export class VaultItemsComponent implements OnInit, OnDestroy { async applyFilter(filter: (cipher: CipherView) => boolean = null) { this.filter = filter; - await this.search(null); - } - - async search(timeout: number = null, indexedCiphers?: CipherView[]) { - this.searchPending = false; - if (this.searchTimeout != null) { - clearTimeout(this.searchTimeout); - } - if (timeout == null) { - await this.doSearch(indexedCiphers); - return; - } - this.searchPending = true; - this.searchTimeout = setTimeout(async () => { - await this.doSearch(indexedCiphers); - this.searchPending = false; - }, timeout); } selectCipher(cipher: CipherView) { @@ -121,25 +123,44 @@ export class VaultItemsComponent implements OnInit, OnDestroy { protected deletedFilter: (cipher: CipherView) => boolean = (c) => c.isDeleted === this.deleted; - protected async doSearch(indexedCiphers?: CipherView[], userId?: UserId) { - // Get userId from activeAccount if not provided from parent stream - if (!userId) { - userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); - } + /** + * Creates stream of dependencies that results in the list of ciphers to display + * within the vault list. + * + * Note: This previously used promises but race conditions with how the ciphers were + * stored in electron. Using observables is more reliable as fresh values will always + * cascade through the components. + */ + private subscribeToCiphers() { + getUserId(this.accountService.activeAccount$) + .pipe( + switchMap((userId) => + combineLatest([ + this.cipherService.cipherViews$(userId).pipe(filter((ciphers) => ciphers != null)), + this.cipherService.failedToDecryptCiphers$(userId), + this._searchText$, + this._filter$, + of(userId), + ]), + ), + switchMap(([indexedCiphers, failedCiphers, searchText, filter, userId]) => { + let allCiphers = indexedCiphers ?? []; + const _failedCiphers = failedCiphers ?? []; - indexedCiphers = - indexedCiphers ?? (await firstValueFrom(this.cipherService.cipherViews$(userId))); + allCiphers = [..._failedCiphers, ...allCiphers]; - const failedCiphers = await firstValueFrom(this.cipherService.failedToDecryptCiphers$(userId)); - if (failedCiphers != null && failedCiphers.length > 0) { - indexedCiphers = [...failedCiphers, ...indexedCiphers]; - } - - this.ciphers = await this.searchService.searchCiphers( - this.userId, - this.searchText, - [this.filter, this.deletedFilter], - indexedCiphers, - ); + return this.searchService.searchCiphers( + userId, + searchText, + [filter, this.deletedFilter], + allCiphers, + ); + }), + takeUntilDestroyed(), + ) + .subscribe((ciphers) => { + this.ciphers = ciphers; + this.loaded = true; + }); } } diff --git a/libs/angular/src/vault/components/view.component.ts b/libs/angular/src/vault/components/view.component.ts index 0dda3c593b7..6b6f24f4217 100644 --- a/libs/angular/src/vault/components/view.component.ts +++ b/libs/angular/src/vault/components/view.component.ts @@ -11,7 +11,17 @@ import { OnInit, Output, } from "@angular/core"; -import { filter, firstValueFrom, map, Observable } from "rxjs"; +import { + BehaviorSubject, + combineLatest, + filter, + firstValueFrom, + map, + Observable, + of, + switchMap, + tap, +} from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; @@ -46,11 +56,22 @@ import { DialogService, ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; import { PasswordRepromptService } from "@bitwarden/vault"; -const BroadcasterSubscriptionId = "ViewComponent"; +const BroadcasterSubscriptionId = "BaseViewComponent"; @Directive() export class ViewComponent implements OnDestroy, OnInit { - @Input() cipherId: string; + /** Observable of cipherId$ that will update each time the `Input` updates */ + private _cipherId$ = new BehaviorSubject(null); + + @Input() + set cipherId(value: string) { + this._cipherId$.next(value); + } + + get cipherId(): string { + return this._cipherId$.getValue(); + } + @Input() collectionId: string; @Output() onEditCipher = new EventEmitter(); @Output() onCloneCipher = new EventEmitter(); @@ -126,13 +147,30 @@ export class ViewComponent implements OnDestroy, OnInit { switch (message.command) { case "syncCompleted": if (message.successfully) { - await this.load(); this.changeDetectorRef.detectChanges(); } break; } }); }); + + // Set up the subscription to the activeAccount$ and cipherId$ observables + combineLatest([this.accountService.activeAccount$.pipe(getUserId), this._cipherId$]) + .pipe( + tap(() => this.cleanUp()), + switchMap(([userId, cipherId]) => { + const cipher$ = this.cipherService.cipherViews$(userId).pipe( + map((ciphers) => ciphers?.find((c) => c.id === cipherId)), + filter((cipher) => !!cipher), + ); + return combineLatest([of(userId), cipher$]); + }), + ) + .subscribe(([userId, cipher]) => { + this.cipher = cipher; + + void this.constructCipherDetails(userId); + }); } ngOnDestroy() { @@ -140,70 +178,6 @@ export class ViewComponent implements OnDestroy, OnInit { this.cleanUp(); } - async load() { - this.cleanUp(); - - // Grab individual cipher from `cipherViews$` for the most up-to-date information - const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - this.cipher = await firstValueFrom( - this.cipherService.cipherViews$(activeUserId).pipe( - map((ciphers) => ciphers?.find((c) => c.id === this.cipherId)), - filter((cipher) => !!cipher), - ), - ); - - this.canAccessPremium = await firstValueFrom( - this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId), - ); - this.showPremiumRequiredTotp = - this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationUseTotp; - this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(this.cipher, [ - this.collectionId as CollectionId, - ]); - this.canRestoreCipher$ = this.cipherAuthorizationService.canRestoreCipher$(this.cipher); - - if (this.cipher.folderId) { - this.folder = await ( - await firstValueFrom(this.folderService.folderViews$(activeUserId)) - ).find((f) => f.id == this.cipher.folderId); - } - - const canGenerateTotp = - this.cipher.type === CipherType.Login && - this.cipher.login.totp && - (this.cipher.organizationUseTotp || this.canAccessPremium); - - this.totpInfo$ = canGenerateTotp - ? this.totpService.getCode$(this.cipher.login.totp).pipe( - map((response) => { - const epoch = Math.round(new Date().getTime() / 1000.0); - const mod = epoch % response.period; - - // Format code - const totpCodeFormatted = - response.code.length > 4 - ? `${response.code.slice(0, Math.floor(response.code.length / 2))} ${response.code.slice(Math.floor(response.code.length / 2))}` - : response.code; - - return { - totpCode: response.code, - totpCodeFormatted, - totpDash: +(Math.round(((78.6 / response.period) * mod + "e+2") as any) + "e-2"), - totpSec: response.period - mod, - totpLow: response.period - mod <= 7, - } as TotpInfo; - }), - ) - : undefined; - - if (this.previousCipherId !== this.cipherId) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.eventCollectionService.collect(EventType.Cipher_ClientViewed, this.cipherId); - } - this.previousCipherId = this.cipherId; - } - async edit() { this.onEditCipher.emit(this.cipher); } @@ -533,4 +507,61 @@ export class ViewComponent implements OnDestroy, OnInit { this.showCardCode = false; this.passwordReprompted = false; } + + /** + * When a cipher is viewed, construct all details for the view that are not directly + * available from the cipher object itself. + */ + private async constructCipherDetails(userId: UserId) { + this.canAccessPremium = await firstValueFrom( + this.billingAccountProfileStateService.hasPremiumFromAnySource$(userId), + ); + this.showPremiumRequiredTotp = + this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationUseTotp; + this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(this.cipher, [ + this.collectionId as CollectionId, + ]); + this.canRestoreCipher$ = this.cipherAuthorizationService.canRestoreCipher$(this.cipher); + + if (this.cipher.folderId) { + this.folder = await ( + await firstValueFrom(this.folderService.folderViews$(userId)) + ).find((f) => f.id == this.cipher.folderId); + } + + const canGenerateTotp = + this.cipher.type === CipherType.Login && + this.cipher.login.totp && + (this.cipher.organizationUseTotp || this.canAccessPremium); + + this.totpInfo$ = canGenerateTotp + ? this.totpService.getCode$(this.cipher.login.totp).pipe( + map((response) => { + const epoch = Math.round(new Date().getTime() / 1000.0); + const mod = epoch % response.period; + + // Format code + const totpCodeFormatted = + response.code.length > 4 + ? `${response.code.slice(0, Math.floor(response.code.length / 2))} ${response.code.slice(Math.floor(response.code.length / 2))}` + : response.code; + + return { + totpCode: response.code, + totpCodeFormatted, + totpDash: +(Math.round(((78.6 / response.period) * mod + "e+2") as any) + "e-2"), + totpSec: response.period - mod, + totpLow: response.period - mod <= 7, + } as TotpInfo; + }), + ) + : undefined; + + if (this.previousCipherId !== this.cipherId) { + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.eventCollectionService.collect(EventType.Cipher_ClientViewed, this.cipherId); + } + this.previousCipherId = this.cipherId; + } } diff --git a/libs/common/src/platform/notifications/internal/default-notifications.service.ts b/libs/common/src/platform/notifications/internal/default-notifications.service.ts index 423b3370455..40c93f8f22a 100644 --- a/libs/common/src/platform/notifications/internal/default-notifications.service.ts +++ b/libs/common/src/platform/notifications/internal/default-notifications.service.ts @@ -153,14 +153,14 @@ export class DefaultNotificationsService implements NotificationsServiceAbstract await this.syncService.syncUpsertCipher( notification.payload as SyncCipherNotification, notification.type === NotificationType.SyncCipherUpdate, - payloadUserId, + userId, ); break; case NotificationType.SyncCipherDelete: case NotificationType.SyncLoginDelete: await this.syncService.syncDeleteCipher( notification.payload as SyncCipherNotification, - payloadUserId, + userId, ); break; case NotificationType.SyncFolderCreate: diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 8d4146eaaba..c192876c83e 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -1,17 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { - combineLatest, - filter, - firstValueFrom, - map, - merge, - Observable, - of, - shareReplay, - Subject, - switchMap, -} from "rxjs"; +import { combineLatest, filter, firstValueFrom, map, Observable, Subject, switchMap } from "rxjs"; import { SemVer } from "semver"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -40,6 +29,7 @@ import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypt import { StateProvider } from "../../platform/state"; import { CipherId, CollectionId, OrganizationId, UserId } from "../../types/guid"; import { OrgKey, UserKey } from "../../types/key"; +import { perUserCache$ } from "../../vault/utils/observable-utilities"; import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service"; import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service"; import { FieldType } from "../enums"; @@ -91,11 +81,12 @@ export class CipherService implements CipherServiceAbstraction { this.sortCiphersByLastUsed, ); /** - * Observable that forces the `cipherViews$` observable to re-emit with the provided value. - * Used to let subscribers of `cipherViews$` know that the decrypted ciphers have been cleared for the active user. + * Observable that forces the `cipherViews$` observable for the given user to emit a null value. + * Used to let subscribers of `cipherViews$` know that the decrypted ciphers have been cleared for the user and to + * clear them from the shareReplay buffer created in perUserCache$(). * @private */ - private forceCipherViews$: Subject = new Subject(); + private clearCipherViewsForUser$: Subject = new Subject(); constructor( private keyService: KeyService, @@ -132,13 +123,16 @@ export class CipherService implements CipherServiceAbstraction { * A `null` value indicates that the latest encrypted ciphers have not been decrypted yet and that * decryption is in progress. The latest decrypted ciphers will be emitted once decryption is complete. */ - cipherViews$(userId: UserId): Observable { - return combineLatest([this.encryptedCiphersState(userId).state$, this.localData$(userId)]).pipe( - filter(([ciphers]) => ciphers != null), // Skip if ciphers haven't been loaded yor synced yet - switchMap(() => merge(this.forceCipherViews$, this.getAllDecrypted(userId))), - shareReplay({ bufferSize: 1, refCount: true }), + cipherViews$ = perUserCache$((userId: UserId): Observable => { + return combineLatest([ + this.encryptedCiphersState(userId).state$, + this.localData$(userId), + this.keyService.cipherDecryptionKeys$(userId, true), + ]).pipe( + filter(([ciphers, keys]) => ciphers != null && keys != null), // Skip if ciphers haven't been loaded yor synced yet + switchMap(() => this.getAllDecrypted(userId)), ); - } + }, this.clearCipherViewsForUser$); addEditCipherInfo$(userId: UserId): Observable { return this.addEditCipherInfoState(userId).state$; @@ -149,13 +143,11 @@ export class CipherService implements CipherServiceAbstraction { * * An empty array indicates that all ciphers were successfully decrypted. */ - failedToDecryptCiphers$(userId: UserId): Observable { + failedToDecryptCiphers$ = perUserCache$((userId: UserId): Observable => { return this.failedToDecryptCiphersState(userId).state$.pipe( filter((ciphers) => ciphers != null), - switchMap((ciphers) => merge(this.forceCipherViews$, of(ciphers))), - shareReplay({ bufferSize: 1, refCount: true }), ); - } + }, this.clearCipherViewsForUser$); async setDecryptedCipherCache(value: CipherView[], userId: UserId) { // Sometimes we might prematurely decrypt the vault and that will result in no ciphers @@ -190,10 +182,8 @@ export class CipherService implements CipherServiceAbstraction { userId ??= activeUserId; await this.clearDecryptedCiphersState(userId); - // Force the cipherView$ observable (which always tracks the active user) to re-emit - if (userId == activeUserId) { - this.forceCipherViews$.next(null); - } + // Force the cached cipherView$ observable(s) to emit a null value + this.clearCipherViewsForUser$.next(userId); } async encrypt( @@ -402,10 +392,14 @@ export class CipherService implements CipherServiceAbstraction { return await this.getDecryptedCiphers(userId); } - const [newDecCiphers, failedCiphers] = await this.decryptCiphers( - await this.getAll(userId), - userId, - ); + const decrypted = await this.decryptCiphers(await this.getAll(userId), userId); + + // We failed to decrypt, return empty array but do not cache + if (decrypted == null) { + return []; + } + + const [newDecCiphers, failedCiphers] = decrypted; await this.setDecryptedCipherCache(newDecCiphers, userId); await this.setFailedDecryptedCiphers(failedCiphers, userId); @@ -429,12 +423,12 @@ export class CipherService implements CipherServiceAbstraction { private async decryptCiphers( ciphers: Cipher[], userId: UserId, - ): Promise<[CipherView[], CipherView[]]> { + ): Promise<[CipherView[], CipherView[]] | null> { const keys = await firstValueFrom(this.keyService.cipherDecryptionKeys$(userId, true)); if (keys == null || (keys.userKey == null && Object.keys(keys.orgKeys).length === 0)) { // return early if there are no keys to decrypt with - return [[], []]; + return null; } // Group ciphers by orgId or under 'null' for the user's ciphers diff --git a/libs/common/src/vault/tasks/services/default-task.service.ts b/libs/common/src/vault/tasks/services/default-task.service.ts index 016eed2e7d6..7386102263c 100644 --- a/libs/common/src/vault/tasks/services/default-task.service.ts +++ b/libs/common/src/vault/tasks/services/default-task.service.ts @@ -12,8 +12,11 @@ import { MessageListener } from "@bitwarden/common/platform/messaging"; import { NotificationsService } from "@bitwarden/common/platform/notifications"; import { StateProvider } from "@bitwarden/common/platform/state"; import { SecurityTaskId, UserId } from "@bitwarden/common/types/guid"; +import { + filterOutNullish, + perUserCache$, +} from "@bitwarden/common/vault/utils/observable-utilities"; -import { filterOutNullish, perUserCache$ } from "../../utils/observable-utilities"; import { TaskService } from "../abstractions/task.service"; import { SecurityTaskStatus } from "../enums"; import { SecurityTask, SecurityTaskData, SecurityTaskResponse } from "../models"; diff --git a/libs/common/src/vault/utils/observable-utilities.ts b/libs/common/src/vault/utils/observable-utilities.ts index bb559c600d3..cdec51fc953 100644 --- a/libs/common/src/vault/utils/observable-utilities.ts +++ b/libs/common/src/vault/utils/observable-utilities.ts @@ -1,20 +1,38 @@ -import { filter, Observable, OperatorFunction, shareReplay } from "rxjs"; +import { EMPTY, filter, map, merge, Observable, OperatorFunction, shareReplay } from "rxjs"; import { UserId } from "@bitwarden/common/types/guid"; /** * Builds an observable once per userId and caches it for future requests. * The built observables are shared among subscribers with a replay buffer size of 1. + * + * Optionally, a clearBuffer$ observable can be provided to clear the replay buffer for a specific or all userIds. * @param create - A function that creates an observable for a given userId. + * @param clearBuffer$ - An observable that, when emitted, clears the buffer for the emitted userId. When null is emitted, all caches are cleared. */ export function perUserCache$( create: (userId: UserId) => Observable, -): (userId: UserId) => Observable { - const cache = new Map>(); + clearBuffer$: Observable, +): (userId: UserId) => Observable; +export function perUserCache$( + create: (userId: UserId) => Observable, +): (userId: UserId) => Observable; +export function perUserCache$( + create: (userId: UserId) => Observable, + clearBuffer$: Observable | undefined = undefined, +): (userId: UserId) => Observable { + const cache = new Map>(); return (userId: UserId) => { let observable = cache.get(userId); if (!observable) { - observable = create(userId).pipe(shareReplay({ bufferSize: 1, refCount: false })); + clearBuffer$ ??= EMPTY; + observable = merge( + create(userId), + clearBuffer$.pipe( + filter((clearId) => clearId === userId || clearId === null), + map(() => null), + ), + ).pipe(shareReplay({ bufferSize: 1, refCount: false })); cache.set(userId, observable); } return observable; From b66430b25cb68fe3f8a99b6f1d2631a3807a199e Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Tue, 15 Apr 2025 16:36:05 -0400 Subject: [PATCH 26/47] [PM-19781] Lit Components icons cleanup (#14294) * update icon shapes to match new design system icons * add AngleUpIcon to storybook * rename Family icon to Users to match design system naming conventions * add Collection icon * move illustrations to their own path/category to match design system convention * remove hardcoded PartyHorn illustration size * fix swapped story names * rename PartyHorn illustration to Celebrate to match design system convention * update Warning illustration to use new design system shape --- .../cipher/cipher-indicator-icons.ts | 4 +- .../content/components/icons/angle-down.ts | 4 +- .../content/components/icons/angle-up.ts | 9 +--- .../content/components/icons/business.ts | 19 ++------ .../content/components/icons/close.ts | 4 +- .../content/components/icons/collection.ts | 23 ++++++++++ .../components/icons/exclamation-triangle.ts | 14 +++++- .../content/components/icons/family.ts | 18 -------- .../content/components/icons/folder.ts | 5 ++- .../content/components/icons/globe.ts | 5 +-- .../content/components/icons/index.ts | 6 +-- .../content/components/icons/pencil-square.ts | 8 +++- .../content/components/icons/shield.ts | 4 +- .../autofill/content/components/icons/user.ts | 4 +- .../content/components/icons/users.ts | 18 ++++++++ .../content/components/icons/warning.ts | 23 ---------- .../celebrate.ts} | 12 ++--- .../content/components/illustrations/index.ts | 3 ++ .../{icons => illustrations}/keyhole.ts | 0 .../components/illustrations/warning.ts | 22 ++++++++++ .../lit-stories/.lit-docs/icons.mdx | 10 ++--- ... => cipher-indicator-icons.lit-stories.ts} | 2 +- .../lit-stories/icons/icons.lit-stories.ts | 10 ++--- .../illustrations.lit-stories.ts | 44 +++++++++++++++++++ .../components/notification/button-row.ts | 4 +- .../notification/confirmation/body.ts | 4 +- 26 files changed, 168 insertions(+), 111 deletions(-) create mode 100644 apps/browser/src/autofill/content/components/icons/collection.ts delete mode 100644 apps/browser/src/autofill/content/components/icons/family.ts create mode 100644 apps/browser/src/autofill/content/components/icons/users.ts delete mode 100644 apps/browser/src/autofill/content/components/icons/warning.ts rename apps/browser/src/autofill/content/components/{icons/party-horn.ts => illustrations/celebrate.ts} (98%) create mode 100644 apps/browser/src/autofill/content/components/illustrations/index.ts rename apps/browser/src/autofill/content/components/{icons => illustrations}/keyhole.ts (100%) create mode 100644 apps/browser/src/autofill/content/components/illustrations/warning.ts rename apps/browser/src/autofill/content/components/lit-stories/ciphers/{cipher-indicator-icon.lit-stories.ts => cipher-indicator-icons.lit-stories.ts} (94%) create mode 100644 apps/browser/src/autofill/content/components/lit-stories/illustrations/illustrations.lit-stories.ts diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts b/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts index 39d4dd28f24..9096149f510 100644 --- a/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts +++ b/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts @@ -4,7 +4,7 @@ import { html } from "lit"; import { Theme } from "@bitwarden/common/platform/enums"; import { themes } from "../../../content/components/constants/styles"; -import { Business, Family } from "../../../content/components/icons"; +import { Business, Users } from "../../../content/components/icons"; // @TODO connect data source to icon checks // @TODO support other indicator types (attachments, etc) @@ -19,7 +19,7 @@ export function CipherInfoIndicatorIcons({ }) { const indicatorIcons = [ ...(showBusinessIcon ? [Business({ color: themes[theme].text.muted, theme })] : []), - ...(showFamilyIcon ? [Family({ color: themes[theme].text.muted, theme })] : []), + ...(showFamilyIcon ? [Users({ color: themes[theme].text.muted, theme })] : []), ]; return indicatorIcons.length diff --git a/apps/browser/src/autofill/content/components/icons/angle-down.ts b/apps/browser/src/autofill/content/components/icons/angle-down.ts index db5275aafa9..27cd5ab81c5 100644 --- a/apps/browser/src/autofill/content/components/icons/angle-down.ts +++ b/apps/browser/src/autofill/content/components/icons/angle-down.ts @@ -8,10 +8,10 @@ export function AngleDown({ color, disabled, theme }: IconProps) { const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; return html` - + `; diff --git a/apps/browser/src/autofill/content/components/icons/angle-up.ts b/apps/browser/src/autofill/content/components/icons/angle-up.ts index 7344123d5ad..f8bda632285 100644 --- a/apps/browser/src/autofill/content/components/icons/angle-up.ts +++ b/apps/browser/src/autofill/content/components/icons/angle-up.ts @@ -8,15 +8,10 @@ export function AngleUp({ color, disabled, theme }: IconProps) { const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; return html` - + `; diff --git a/apps/browser/src/autofill/content/components/icons/business.ts b/apps/browser/src/autofill/content/components/icons/business.ts index ef8e082c21f..79e64a0a1f9 100644 --- a/apps/browser/src/autofill/content/components/icons/business.ts +++ b/apps/browser/src/autofill/content/components/icons/business.ts @@ -8,30 +8,17 @@ export function Business({ color, disabled, theme }: IconProps) { const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; return html` - + - - `; } diff --git a/apps/browser/src/autofill/content/components/icons/close.ts b/apps/browser/src/autofill/content/components/icons/close.ts index c9d9286ca3f..27610bc7773 100644 --- a/apps/browser/src/autofill/content/components/icons/close.ts +++ b/apps/browser/src/autofill/content/components/icons/close.ts @@ -8,10 +8,10 @@ export function Close({ color, disabled, theme }: IconProps) { const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; return html` - + `; diff --git a/apps/browser/src/autofill/content/components/icons/collection.ts b/apps/browser/src/autofill/content/components/icons/collection.ts new file mode 100644 index 00000000000..fb2c58647c5 --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/collection.ts @@ -0,0 +1,23 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { IconProps } from "../common-types"; +import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; + +export function Collection({ color, disabled, theme }: IconProps) { + const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; + + return html` + + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/icons/exclamation-triangle.ts b/apps/browser/src/autofill/content/components/icons/exclamation-triangle.ts index d87d5621e30..c4f587b2d7b 100644 --- a/apps/browser/src/autofill/content/components/icons/exclamation-triangle.ts +++ b/apps/browser/src/autofill/content/components/icons/exclamation-triangle.ts @@ -8,10 +8,20 @@ export function ExclamationTriangle({ color, disabled, theme }: IconProps) { const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; return html` - + + + `; diff --git a/apps/browser/src/autofill/content/components/icons/family.ts b/apps/browser/src/autofill/content/components/icons/family.ts deleted file mode 100644 index 9870c5d37c0..00000000000 --- a/apps/browser/src/autofill/content/components/icons/family.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { css } from "@emotion/css"; -import { html } from "lit"; - -import { IconProps } from "../common-types"; -import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; - -export function Family({ color, disabled, theme }: IconProps) { - const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; - - return html` - - - - `; -} diff --git a/apps/browser/src/autofill/content/components/icons/folder.ts b/apps/browser/src/autofill/content/components/icons/folder.ts index 84577aef820..1b93d2d32ea 100644 --- a/apps/browser/src/autofill/content/components/icons/folder.ts +++ b/apps/browser/src/autofill/content/components/icons/folder.ts @@ -8,10 +8,11 @@ export function Folder({ color, disabled, theme }: IconProps) { const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; return html` - + `; diff --git a/apps/browser/src/autofill/content/components/icons/globe.ts b/apps/browser/src/autofill/content/components/icons/globe.ts index fc0a975284d..936fd8d2802 100644 --- a/apps/browser/src/autofill/content/components/icons/globe.ts +++ b/apps/browser/src/autofill/content/components/icons/globe.ts @@ -8,11 +8,10 @@ export function Globe({ color, disabled, theme }: IconProps) { const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; return html` - + `; diff --git a/apps/browser/src/autofill/content/components/icons/index.ts b/apps/browser/src/autofill/content/components/icons/index.ts index 4b6cb7abdd8..de39b70ab24 100644 --- a/apps/browser/src/autofill/content/components/icons/index.ts +++ b/apps/browser/src/autofill/content/components/icons/index.ts @@ -3,14 +3,12 @@ export { AngleUp } from "./angle-up"; export { BrandIconContainer } from "./brand-icon-container"; export { Business } from "./business"; export { Close } from "./close"; +export { Collection } from "./collection"; export { ExclamationTriangle } from "./exclamation-triangle"; export { ExternalLink } from "./external-link"; -export { Family } from "./family"; export { Folder } from "./folder"; export { Globe } from "./globe"; -export { Keyhole } from "./keyhole"; -export { PartyHorn } from "./party-horn"; export { PencilSquare } from "./pencil-square"; export { Shield } from "./shield"; export { User } from "./user"; -export { Warning } from "./warning"; +export { Users } from "./users"; diff --git a/apps/browser/src/autofill/content/components/icons/pencil-square.ts b/apps/browser/src/autofill/content/components/icons/pencil-square.ts index f41ab927809..11366f2631a 100644 --- a/apps/browser/src/autofill/content/components/icons/pencil-square.ts +++ b/apps/browser/src/autofill/content/components/icons/pencil-square.ts @@ -8,10 +8,14 @@ export function PencilSquare({ color, disabled, theme }: IconProps) { const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; return html` - + + `; diff --git a/apps/browser/src/autofill/content/components/icons/shield.ts b/apps/browser/src/autofill/content/components/icons/shield.ts index 5a2d7d39d58..a027dd3e113 100644 --- a/apps/browser/src/autofill/content/components/icons/shield.ts +++ b/apps/browser/src/autofill/content/components/icons/shield.ts @@ -8,10 +8,10 @@ export function Shield({ color, theme }: IconProps) { const shapeColor = color || themes[theme].brandLogo; return html` - + `; diff --git a/apps/browser/src/autofill/content/components/icons/user.ts b/apps/browser/src/autofill/content/components/icons/user.ts index 32ccd3a2031..b59204a0ad8 100644 --- a/apps/browser/src/autofill/content/components/icons/user.ts +++ b/apps/browser/src/autofill/content/components/icons/user.ts @@ -8,10 +8,10 @@ export function User({ color, disabled, theme }: IconProps) { const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; return html` - + `; diff --git a/apps/browser/src/autofill/content/components/icons/users.ts b/apps/browser/src/autofill/content/components/icons/users.ts new file mode 100644 index 00000000000..eb7840104f0 --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/users.ts @@ -0,0 +1,18 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { IconProps } from "../common-types"; +import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; + +export function Users({ color, disabled, theme }: IconProps) { + const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; + + return html` + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/icons/warning.ts b/apps/browser/src/autofill/content/components/icons/warning.ts deleted file mode 100644 index 9ae9aeca352..00000000000 --- a/apps/browser/src/autofill/content/components/icons/warning.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { html } from "lit"; - -// This icon has static multi-colors for each theme -export function Warning() { - return html` - - - - - - `; -} diff --git a/apps/browser/src/autofill/content/components/icons/party-horn.ts b/apps/browser/src/autofill/content/components/illustrations/celebrate.ts similarity index 98% rename from apps/browser/src/autofill/content/components/icons/party-horn.ts rename to apps/browser/src/autofill/content/components/illustrations/celebrate.ts index 439d60a79de..30b3743004f 100644 --- a/apps/browser/src/autofill/content/components/icons/party-horn.ts +++ b/apps/browser/src/autofill/content/components/illustrations/celebrate.ts @@ -5,16 +5,10 @@ import { ThemeTypes } from "@bitwarden/common/platform/enums"; import { IconProps } from "../common-types"; // This icon has static multi-colors for each theme -export function PartyHorn({ theme }: IconProps) { +export function Celebrate({ theme }: IconProps) { if (theme === ThemeTypes.Dark) { return html` - + + + + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/icons.mdx b/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/icons.mdx index f85cf3ae90f..571ed10285a 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/icons.mdx +++ b/apps/browser/src/autofill/content/components/lit-stories/.lit-docs/icons.mdx @@ -2,7 +2,7 @@ import { Meta, Controls } from "@storybook/addon-docs"; import * as stories from "./icons.lit-stories"; - + ## Icon Stories @@ -14,12 +14,12 @@ like size, color, and theme. Each story is an example of how a specific icon can | | | | ------------------------- | ------------------ | -| `AngleDownIcon` | `FolderIcon` | -| `BusinessIcon` | `GlobeIcon` | -| `BrandIcon` | `PartyHornIcon` | +| `AngleDownIcon` | `AngleUpIcon` | +| `BusinessIcon` | `FolderIcon` | +| `BrandIcon` | `GlobeIcon` | | `CloseIcon` | `PencilSquareIcon` | | `ExclamationTriangleIcon` | `ShieldIcon` | -| `FamilyIcon` | `UserIcon` | +| `UsersIcon` | `UserIcon` | ## Props diff --git a/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-indicator-icon.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-indicator-icons.lit-stories.ts similarity index 94% rename from apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-indicator-icon.lit-stories.ts rename to apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-indicator-icons.lit-stories.ts index 89c3ecbcb1c..08530452730 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-indicator-icon.lit-stories.ts +++ b/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-indicator-icons.lit-stories.ts @@ -12,7 +12,7 @@ type Args = { }; export default { - title: "Components/Ciphers/Cipher Indicator Icon", + title: "Components/Ciphers/Cipher Indicator Icons", argTypes: { showBusinessIcon: { control: "boolean" }, showFamilyIcon: { control: "boolean" }, diff --git a/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts index 8bd87ef6674..fc5db1c7c2c 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts +++ b/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts @@ -14,7 +14,7 @@ type Args = { }; export default { - title: "Components/Icons/Icons", + title: "Components/Icons", argTypes: { iconLink: { control: "text" }, color: { control: "color" }, @@ -53,16 +53,16 @@ const createIconStory = (iconName: keyof typeof Icons): StoryObj => { }; export const AngleDownIcon = createIconStory("AngleDown"); -export const BusinessIcon = createIconStory("Business"); +export const AngleUpIcon = createIconStory("AngleUp"); export const BrandIcon = createIconStory("BrandIconContainer"); +export const BusinessIcon = createIconStory("Business"); export const CloseIcon = createIconStory("Close"); +export const CollectionIcon = createIconStory("Collection"); export const ExclamationTriangleIcon = createIconStory("ExclamationTriangle"); export const ExternalLinkIcon = createIconStory("ExternalLink"); -export const FamilyIcon = createIconStory("Family"); export const FolderIcon = createIconStory("Folder"); export const GlobeIcon = createIconStory("Globe"); -export const KeyholeIcon = createIconStory("Keyhole"); -export const PartyHornIcon = createIconStory("PartyHorn"); export const PencilSquareIcon = createIconStory("PencilSquare"); export const ShieldIcon = createIconStory("Shield"); export const UserIcon = createIconStory("User"); +export const UsersIcon = createIconStory("Users"); diff --git a/apps/browser/src/autofill/content/components/lit-stories/illustrations/illustrations.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/illustrations/illustrations.lit-stories.ts new file mode 100644 index 00000000000..86d55f2f795 --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/illustrations/illustrations.lit-stories.ts @@ -0,0 +1,44 @@ +import { Meta, StoryObj } from "@storybook/web-components"; +import { html } from "lit"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; + +import * as Illustrations from "../../illustrations"; + +type Args = { + theme: Theme; + size: number; +}; + +export default { + title: "Components/Illustrations", + argTypes: { + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + size: { control: "number", min: 10, max: 100, step: 1 }, + }, + args: { + theme: ThemeTypes.Light, + size: 50, + }, +} as Meta; + +const Template = ( + args: Args, + IllustrationComponent: (props: Args) => ReturnType, +) => html` +
+ ${IllustrationComponent({ ...args })} +
+`; + +const createIllustrationStory = (illustrationName: keyof typeof Illustrations): StoryObj => { + return { + render: (args) => Template(args, Illustrations[illustrationName]), + } as StoryObj; +}; + +export const KeyholeIllustration = createIllustrationStory("Keyhole"); +export const CelebrateIllustration = createIllustrationStory("Celebrate"); +export const WarningIllustration = createIllustrationStory("Warning"); diff --git a/apps/browser/src/autofill/content/components/notification/button-row.ts b/apps/browser/src/autofill/content/components/notification/button-row.ts index 8661f5957e1..6fa32f11aa2 100644 --- a/apps/browser/src/autofill/content/components/notification/button-row.ts +++ b/apps/browser/src/autofill/content/components/notification/button-row.ts @@ -4,14 +4,14 @@ import { ProductTierType } from "@bitwarden/common/billing/enums"; import { Theme } from "@bitwarden/common/platform/enums"; import { Option, OrgView, FolderView } from "../common-types"; -import { Business, Family, Folder, User } from "../icons"; +import { Business, Users, Folder, User } from "../icons"; import { ButtonRow } from "../rows/button-row"; function getVaultIconByProductTier(productTierType?: ProductTierType): Option["icon"] { switch (productTierType) { case ProductTierType.Free: case ProductTierType.Families: - return Family; + return Users; case ProductTierType.Teams: case ProductTierType.Enterprise: case ProductTierType.TeamsStarter: diff --git a/apps/browser/src/autofill/content/components/notification/confirmation/body.ts b/apps/browser/src/autofill/content/components/notification/confirmation/body.ts index 55d257b36f4..d2ac7f36277 100644 --- a/apps/browser/src/autofill/content/components/notification/confirmation/body.ts +++ b/apps/browser/src/autofill/content/components/notification/confirmation/body.ts @@ -4,7 +4,7 @@ import { html, nothing } from "lit"; import { Theme } from "@bitwarden/common/platform/enums"; import { themes } from "../../constants/styles"; -import { PartyHorn, Keyhole, Warning } from "../../icons"; +import { Celebrate, Keyhole, Warning } from "../../illustrations"; import { NotificationConfirmationMessage } from "./message"; @@ -33,7 +33,7 @@ export function NotificationConfirmationBody({ theme, handleOpenVault, }: NotificationConfirmationBodyProps) { - const IconComponent = tasksAreComplete ? Keyhole : !error ? PartyHorn : Warning; + const IconComponent = tasksAreComplete ? Keyhole : !error ? Celebrate : Warning; const showConfirmationMessage = confirmationMessage || buttonText || messageDetails; From a61d8780816c0c57710f76ff331e1635b15e247a Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Tue, 15 Apr 2025 17:19:58 -0400 Subject: [PATCH 27/47] PM-20106 Pass indicator data to notification bar cipher items (#14246) * PM-20106 initial approach whihc preserves exisiting indicator file style * refactored approach to be able to pass any icon when or if needed in the future * address feedback --- .../background/notification.background.ts | 31 ++++++++++++++-- .../cipher/cipher-indicator-icons.ts | 35 +++++++++++-------- .../content/components/cipher/cipher-info.ts | 16 ++++++--- .../content/components/cipher/types.ts | 9 +++++ 4 files changed, 70 insertions(+), 21 deletions(-) diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 1f0cc469e2c..6589252d94b 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -16,6 +16,7 @@ import { } from "@bitwarden/common/autofill/constants"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service"; +import { ProductTierType } from "@bitwarden/common/billing/enums/product-tier-type.enum"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -41,7 +42,11 @@ import { SecurityTask } from "@bitwarden/common/vault/tasks/models/security-task import { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window"; import { BrowserApi } from "../../platform/browser/browser-api"; import { openAddEditVaultItemPopout } from "../../vault/popup/utils/vault-popout-window"; -import { NotificationCipherData } from "../content/components/cipher/types"; +import { + OrganizationCategory, + OrganizationCategories, + NotificationCipherData, +} from "../content/components/cipher/types"; import { NotificationQueueMessageType } from "../enums/notification-queue-message-type.enum"; import { AutofillService } from "../services/abstractions/autofill.service"; @@ -174,8 +179,29 @@ export default class NotificationBackground { activeUserId, ); + const organizations = await firstValueFrom( + this.organizationService.organizations$(activeUserId), + ); + return decryptedCiphers.map((view) => { - const { id, name, reprompt, favorite, login } = view; + const { id, name, reprompt, favorite, login, organizationId } = view; + + const organizationType = organizationId + ? organizations.find((org) => org.id === organizationId)?.productTierType + : null; + + const organizationCategories: OrganizationCategory[] = []; + + if ( + [ProductTierType.Teams, ProductTierType.Enterprise, ProductTierType.TeamsStarter].includes( + organizationType, + ) + ) { + organizationCategories.push(OrganizationCategories.business); + } + if ([ProductTierType.Families, ProductTierType.Free].includes(organizationType)) { + organizationCategories.push(OrganizationCategories.family); + } return { id, @@ -183,6 +209,7 @@ export default class NotificationBackground { type: CipherType.Login, reprompt, favorite, + ...(organizationCategories.length ? { organizationCategories } : {}), icon: buildCipherIcon(iconsServerUrl, view, showFavicons), login: login && { username: login.username, diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts b/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts index 9096149f510..e4fe012a678 100644 --- a/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts +++ b/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts @@ -1,30 +1,35 @@ import { css } from "@emotion/css"; -import { html } from "lit"; +import { html, TemplateResult } from "lit"; import { Theme } from "@bitwarden/common/platform/enums"; import { themes } from "../../../content/components/constants/styles"; import { Business, Users } from "../../../content/components/icons"; -// @TODO connect data source to icon checks -// @TODO support other indicator types (attachments, etc) +import { OrganizationCategories, OrganizationCategory } from "./types"; + +const cipherIndicatorIconsMap: Record< + OrganizationCategory, + (args: { color: string; theme: Theme }) => TemplateResult +> = { + [OrganizationCategories.business]: Business, + [OrganizationCategories.family]: Users, +}; + export function CipherInfoIndicatorIcons({ - showBusinessIcon, - showFamilyIcon, + organizationCategories = [], theme, }: { - showBusinessIcon?: boolean; - showFamilyIcon?: boolean; + organizationCategories?: OrganizationCategory[]; theme: Theme; }) { - const indicatorIcons = [ - ...(showBusinessIcon ? [Business({ color: themes[theme].text.muted, theme })] : []), - ...(showFamilyIcon ? [Users({ color: themes[theme].text.muted, theme })] : []), - ]; - - return indicatorIcons.length - ? html` ${indicatorIcons} ` - : null; // @TODO null case should be handled by parent + return html` + + ${organizationCategories.map((name) => + cipherIndicatorIconsMap[name]?.({ color: themes[theme].text.muted, theme }), + )} + + `; } const cipherInfoIndicatorIconsStyles = css` diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-info.ts b/apps/browser/src/autofill/content/components/cipher/cipher-info.ts index 6ff32353938..e3d237b9bc6 100644 --- a/apps/browser/src/autofill/content/components/cipher/cipher-info.ts +++ b/apps/browser/src/autofill/content/components/cipher/cipher-info.ts @@ -1,5 +1,5 @@ import { css } from "@emotion/css"; -import { html } from "lit"; +import { html, nothing } from "lit"; import { Theme } from "@bitwarden/common/platform/enums"; @@ -8,14 +8,22 @@ import { themes, typography } from "../../../content/components/constants/styles import { CipherInfoIndicatorIcons } from "./cipher-indicator-icons"; import { NotificationCipherData } from "./types"; -// @TODO support other cipher types (card, identity, notes, etc) export function CipherInfo({ cipher, theme }: { cipher: NotificationCipherData; theme: Theme }) { - const { name, login } = cipher; + const { name, login, organizationCategories } = cipher; + const hasIndicatorIcons = organizationCategories?.length; return html`
- ${[name, CipherInfoIndicatorIcons({ theme })]} + ${[ + name, + hasIndicatorIcons + ? CipherInfoIndicatorIcons({ + theme, + organizationCategories, + }) + : nothing, + ]} ${login?.username diff --git a/apps/browser/src/autofill/content/components/cipher/types.ts b/apps/browser/src/autofill/content/components/cipher/types.ts index ff29f9b559f..590311682bf 100644 --- a/apps/browser/src/autofill/content/components/cipher/types.ts +++ b/apps/browser/src/autofill/content/components/cipher/types.ts @@ -14,6 +14,14 @@ export const CipherRepromptTypes = { type CipherRepromptType = (typeof CipherRepromptTypes)[keyof typeof CipherRepromptTypes]; +export type OrganizationCategory = + (typeof OrganizationCategories)[keyof typeof OrganizationCategories]; + +export const OrganizationCategories = { + business: "business", + family: "family", +} as const; + export type WebsiteIconData = { imageEnabled: boolean; image: string; @@ -50,4 +58,5 @@ export type NotificationCipherData = BaseCipherData & login?: { username: string; }; + organizationCategories?: OrganizationCategory[]; }; From cb869484239c01075e6145681c240c8c43f21a4c Mon Sep 17 00:00:00 2001 From: Miles Blackwood Date: Tue, 15 Apr 2025 20:00:08 -0400 Subject: [PATCH 28/47] [PM-15436] Standalone password entry should trigger save to bitwarden prompt. (#14110) * Modify behavior so standalone password entry (with or without generator) should trigger save to bitwarden prompt. * Rename intent to action, extend button/action styles. * Ensure font weight is returned to normal. * Make save login message a button to handle accessibility, adds helper function. * Fix failing snapshot by reintigrating erroneously removed line. * Update snapshot to match new saveLoginButton. * Add add'l open in new window message to aria label. * Update snapshot with open in new window message. --- apps/browser/src/_locales/en/messages.json | 4 +- .../autofill/background/overlay.background.ts | 6 +-- .../autofill-inline-menu-list.spec.ts.snap | 39 ++------------- .../list/autofill-inline-menu-list.spec.ts | 10 ++-- .../pages/list/autofill-inline-menu-list.ts | 50 +++++++++++++++---- .../overlay/inline-menu/pages/list/list.scss | 8 +++ 6 files changed, 61 insertions(+), 56 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index f3b85496b75..87b94650b51 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -4928,8 +4928,8 @@ "message": "Password regenerated", "description": "Notification message for when a password has been regenerated" }, - "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "saveToBitwarden": { + "message": "Save to Bitwarden", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index a2088f50a11..4e2e773a0c7 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -1852,7 +1852,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { /** * Verifies whether the save login inline menu view should be shown. This requires that - * the login data on the page contains a username and either a current or new password. + * the login data on the page contains either a current or new password. * * @param tab - The tab to check for login data */ @@ -1869,7 +1869,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { return ( (this.shouldShowInlineMenuAccountCreation() || this.focusedFieldMatchesFillType(InlineMenuFillType.PasswordGeneration)) && - !!(loginData.username && (loginData.password || loginData.newPassword)) + !!(loginData.password || loginData.newPassword) ); } @@ -2157,7 +2157,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { "passwordRegenerated", "passwords", "regeneratePassword", - "saveLoginToBitwarden", + "saveToBitwarden", "toggleBitwardenVaultOverlay", "totpCodeAria", "totpSecondsSpanAria", diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap b/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap index acd06fb8c65..b6e41c448d6 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap @@ -4,47 +4,14 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList creates the build sav
- `; diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts index b1eebd2bc39..ed28375e4fe 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts @@ -1089,12 +1089,12 @@ describe("AutofillInlineMenuList", () => { }); describe("displaying the save login view", () => { - let buildSaveLoginInlineMenuListSpy: jest.SpyInstance; + let buildSaveLoginInlineMenuSpy: jest.SpyInstance; beforeEach(() => { - buildSaveLoginInlineMenuListSpy = jest.spyOn( + buildSaveLoginInlineMenuSpy = jest.spyOn( autofillInlineMenuList as any, - "buildSaveLoginInlineMenuList", + "buildSaveLoginInlineMenu", ); }); @@ -1108,7 +1108,7 @@ describe("AutofillInlineMenuList", () => { postWindowMessage({ command: "showSaveLoginInlineMenuList" }); - expect(buildSaveLoginInlineMenuListSpy).not.toHaveBeenCalled(); + expect(buildSaveLoginInlineMenuSpy).not.toHaveBeenCalled(); }); it("builds the save login item view", async () => { @@ -1117,7 +1117,7 @@ describe("AutofillInlineMenuList", () => { postWindowMessage({ command: "showSaveLoginInlineMenuList" }); - expect(buildSaveLoginInlineMenuListSpy).toHaveBeenCalled(); + expect(buildSaveLoginInlineMenuSpy).toHaveBeenCalled(); }); }); diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts index acb01594cc6..e0db93b6b4a 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.ts @@ -3,6 +3,8 @@ import "@webcomponents/custom-elements"; import "lit/polyfill-support.js"; +import { FocusableElement } from "tabbable"; + import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { EVENTS, UPDATE_PASSKEYS_HEADINGS_ON_SCROLL } from "@bitwarden/common/autofill/constants"; import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; @@ -117,7 +119,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { } if (showSaveLoginMenu) { - this.buildSaveLoginInlineMenuList(); + this.buildSaveLoginInlineMenu(); return; } @@ -165,24 +167,52 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { /** * Builds the inline menu list as a prompt that asks the user if they'd like to save the login data. */ - private buildSaveLoginInlineMenuList() { - const saveLoginMessage = globalThis.document.createElement("div"); - saveLoginMessage.classList.add("save-login", "inline-menu-list-message"); - saveLoginMessage.textContent = this.getTranslation("saveLoginToBitwarden"); + private buildSaveLoginInlineMenu() { + const saveLoginButton = globalThis.document.createElement("button"); + saveLoginButton.classList.add( + "save-login", + "inline-menu-list-button", + "inline-menu-list-action", + ); + + saveLoginButton.tabIndex = -1; + saveLoginButton.setAttribute( + "aria-label", + `${this.getTranslation("saveToBitwarden")}, ${this.getTranslation("opensInANewWindow")}`, + ); + saveLoginButton.textContent = this.getTranslation("saveToBitwarden"); + + saveLoginButton.addEventListener(EVENTS.CLICK, this.handleNewLoginVaultItemAction); + saveLoginButton.addEventListener(EVENTS.KEYUP, this.handleSaveLoginInlineMenuKeyUp); + + const inlineMenuListButtonContainer = this.buildButtonContainer(saveLoginButton); - const newItemButton = this.buildNewItemButton(true); this.showInlineMenuAccountCreation = true; - this.inlineMenuListContainer.append(saveLoginMessage, newItemButton); + this.inlineMenuListContainer.append(inlineMenuListButtonContainer); } + private handleSaveLoginInlineMenuKeyUp = (event: KeyboardEvent) => { + const listenedForKeys = new Set(["ArrowDown"]); + if (!listenedForKeys.has(event.code) || !(event.target instanceof Element)) { + return; + } + + event.preventDefault(); + + if (event.code === "ArrowDown") { + (event.target as FocusableElement).focus(); + return; + } + }; + /** * Handles the show save login inline menu list message that is triggered from the background script. */ private handleShowSaveLoginInlineMenuList() { if (this.authStatus === AuthenticationStatus.Unlocked) { this.resetInlineMenuContainer(); - this.buildSaveLoginInlineMenuList(); + this.buildSaveLoginInlineMenu(); } } @@ -521,7 +551,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { this.newItemButtonElement.textContent = this.getNewItemButtonText(showLogin); this.newItemButtonElement.setAttribute("aria-label", this.getNewItemAriaLabel(showLogin)); this.newItemButtonElement.prepend(buildSvgDomElement(plusIcon)); - this.newItemButtonElement.addEventListener(EVENTS.CLICK, this.handeNewItemButtonClick); + this.newItemButtonElement.addEventListener(EVENTS.CLICK, this.handleNewLoginVaultItemAction); return this.buildButtonContainer(this.newItemButtonElement); } @@ -581,7 +611,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { * Handles the click event for the new item button. * Sends a message to the parent window to add a new vault item. */ - private handeNewItemButtonClick = () => { + private handleNewLoginVaultItemAction = () => { let addNewCipherType = this.inlineMenuFillType; if (this.showInlineMenuAccountCreation) { diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/list/list.scss b/apps/browser/src/autofill/overlay/inline-menu/pages/list/list.scss index d0875cfe427..93f5f647ffe 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/list/list.scss +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/list/list.scss @@ -45,6 +45,14 @@ body * { &.no-items, &.save-login { font-size: 1.6rem; + &:has(:focus-visible) { + outline-width: 0.2rem; + outline-style: solid; + + @include themify($themes) { + outline-color: themed("focusOutlineColor"); + } + } } } From 9da15601be3b8a7e2883db55b7ab0a0a8150ccb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ch=C4=99ci=C5=84ski?= Date: Wed, 16 Apr 2025 15:06:41 +0200 Subject: [PATCH 29/47] Add workflow to trigger self-host unified build in publish web (#14268) --- .github/workflows/publish-web.yml | 33 +++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/.github/workflows/publish-web.yml b/.github/workflows/publish-web.yml index 09f5ddc6318..69b29086d36 100644 --- a/.github/workflows/publish-web.yml +++ b/.github/workflows/publish-web.yml @@ -141,3 +141,36 @@ jobs: - name: Log out of Docker run: docker logout + + self-host-unified-build: + name: Trigger self-host unified build + runs-on: ubuntu-22.04 + needs: + - setup + steps: + - name: Log in to Azure - CI subscription + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + with: + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + + - name: Retrieve GitHub PAT secrets + id: retrieve-secret-pat + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: "bitwarden-ci" + secrets: "github-pat-bitwarden-devops-bot-repo-scope" + + - name: Trigger self-host build + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }} + script: | + await github.rest.actions.createWorkflowDispatch({ + owner: 'bitwarden', + repo: 'self-host', + workflow_id: 'build-unified.yml', + ref: 'main', + inputs: { + use_latest_core_version: true + } + }); From 9cffc3b4f4c61aa57dcc62b2298f4840e3906a18 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Wed, 16 Apr 2025 08:16:40 -0500 Subject: [PATCH 30/47] [PM-20118] Capitalize risk insights (#14291) --- apps/web/src/locales/en/messages.json | 4 ++-- .../access-intelligence/risk-insights-loading.component.html | 2 +- .../tools/access-intelligence/risk-insights.component.html | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 0193fc4862b..85a7b8cb927 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -4073,8 +4073,8 @@ "updateBrowser": { "message": "Update browser" }, - "generatingRiskInsights": { - "message": "Generating your risk insights..." + "generatingYourRiskInsights": { + "message": "Generating your Risk Insights..." }, "updateBrowserDesc": { "message": "You are using an unsupported web browser. The web vault may not function properly." diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.html index 4e77838229e..0c5b74eead2 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.html +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.html @@ -4,5 +4,5 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > -

{{ "generatingRiskInsights" | i18n }}

+

{{ "generatingYourRiskInsights" | i18n }}

diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html index 397e2a630de..2d5693dad54 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html @@ -8,6 +8,7 @@ {{ "reviewAtRiskPasswords" | i18n }}
Date: Wed, 16 Apr 2025 11:04:31 -0400 Subject: [PATCH 31/47] fix restore button (#14244) --- apps/desktop/src/vault/app/vault/view.component.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/vault/app/vault/view.component.html b/apps/desktop/src/vault/app/vault/view.component.html index ede6eb7ed82..8477a588fef 100644 --- a/apps/desktop/src/vault/app/vault/view.component.html +++ b/apps/desktop/src/vault/app/vault/view.component.html @@ -656,7 +656,11 @@ class="primary" (click)="restore()" appA11yTitle="{{ 'restore' | i18n }}" - *ngIf="(limitItemDeletion$ | async) ? (canRestoreCipher$ | async) : cipher.isDeleted" + *ngIf=" + (limitItemDeletion$ | async) + ? (canRestoreCipher$ | async) && cipher.isDeleted + : cipher.isDeleted + " > From b413272bd5adc63b2c77bdeceddf23d7f0603208 Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Wed, 16 Apr 2025 11:08:51 -0400 Subject: [PATCH 32/47] [PM-20325] - Misc design fixes/tweaks (#14309) * fix icon sizing in option selection * fix close button vertical centering * fix cipher item update text * fix missing header background color * fix brand logo positioning in notification header --- .../autofill/content/components/buttons/close-button.ts | 1 + .../components/buttons/option-selection-button.ts | 5 +++-- .../autofill/content/components/cipher/cipher-action.ts | 4 ++-- .../content/components/icons/brand-icon-container.ts | 9 +++++++-- .../autofill/content/components/notification/header.ts | 2 +- .../content/components/option-selection/option-item.ts | 7 ++++--- 6 files changed, 18 insertions(+), 10 deletions(-) diff --git a/apps/browser/src/autofill/content/components/buttons/close-button.ts b/apps/browser/src/autofill/content/components/buttons/close-button.ts index c32d0c130e3..05a12d4f453 100644 --- a/apps/browser/src/autofill/content/components/buttons/close-button.ts +++ b/apps/browser/src/autofill/content/components/buttons/close-button.ts @@ -35,5 +35,6 @@ const closeButtonStyles = (theme: Theme) => css` > svg { width: 20px; height: 20px; + vertical-align: middle; } `; diff --git a/apps/browser/src/autofill/content/components/buttons/option-selection-button.ts b/apps/browser/src/autofill/content/components/buttons/option-selection-button.ts index cf9a561ee39..e3c7e0d54e6 100644 --- a/apps/browser/src/autofill/content/components/buttons/option-selection-button.ts +++ b/apps/browser/src/autofill/content/components/buttons/option-selection-button.ts @@ -44,7 +44,7 @@ export function OptionSelectionButton({ `; } -const iconSize = "15px"; +const iconSize = "16px"; const selectionButtonStyles = ({ disabled, @@ -94,7 +94,8 @@ const selectionButtonStyles = ({ > svg { max-width: ${iconSize}; - height: fit-content; + max-height: ${iconSize}; + height: auto; } `; diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-action.ts b/apps/browser/src/autofill/content/components/cipher/cipher-action.ts index 2d386d34d6a..aaa4b11d8a2 100644 --- a/apps/browser/src/autofill/content/components/cipher/cipher-action.ts +++ b/apps/browser/src/autofill/content/components/cipher/cipher-action.ts @@ -19,13 +19,13 @@ export function CipherAction({ ? BadgeButton({ buttonAction: handleAction, // @TODO localize - buttonText: "Update item", + buttonText: "Update", theme, }) : EditButton({ buttonAction: handleAction, // @TODO localize - buttonText: "Edit item", + buttonText: "Edit", theme, }); } diff --git a/apps/browser/src/autofill/content/components/icons/brand-icon-container.ts b/apps/browser/src/autofill/content/components/icons/brand-icon-container.ts index 8df68d79b6e..1b08f261eb6 100644 --- a/apps/browser/src/autofill/content/components/icons/brand-icon-container.ts +++ b/apps/browser/src/autofill/content/components/icons/brand-icon-container.ts @@ -12,8 +12,13 @@ export function BrandIconContainer({ iconLink, theme }: { iconLink?: URL; theme: } const brandIconContainerStyles = css` + display: flex; + justify-content: center; + width: 24px; + height: 24px; + > svg { - width: 20px; - height: fit-content; + width: auto; + height: 100%; } `; diff --git a/apps/browser/src/autofill/content/components/notification/header.ts b/apps/browser/src/autofill/content/components/notification/header.ts index 50c2c629942..d6cedf6a85a 100644 --- a/apps/browser/src/autofill/content/components/notification/header.ts +++ b/apps/browser/src/autofill/content/components/notification/header.ts @@ -49,7 +49,7 @@ const notificationHeaderStyles = ({ display: flex; align-items: center; justify-content: flex-start; - background-color: ${themes[theme].background}; + background-color: ${themes[theme].background.DEFAULT}; padding: 12px 16px 8px 16px; white-space: nowrap; diff --git a/apps/browser/src/autofill/content/components/option-selection/option-item.ts b/apps/browser/src/autofill/content/components/option-selection/option-item.ts index 619d77e63d3..e8a293e2c3f 100644 --- a/apps/browser/src/autofill/content/components/option-selection/option-item.ts +++ b/apps/browser/src/autofill/content/components/option-selection/option-item.ts @@ -62,14 +62,15 @@ const optionItemStyles = css` `; const optionItemIconContainerStyles = css` + display: flex; flex-grow: 1; flex-shrink: 1; - width: ${optionItemIconWidth}px; - height: ${optionItemIconWidth}px; + max-width: ${optionItemIconWidth}px; + max-height: ${optionItemIconWidth}px; > svg { width: 100%; - height: fit-content; + height: auto; } `; From f293c15f4d8ae9bdae07e1f8a26232da2c456243 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Wed, 16 Apr 2025 08:24:30 -0700 Subject: [PATCH 33/47] [PM-19538] Add shareReplay to internal orgKeys subscription (#14034) --- .../collections/services/default-collection.service.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libs/admin-console/src/common/collections/services/default-collection.service.ts b/libs/admin-console/src/common/collections/services/default-collection.service.ts index da50a25886e..1ae58d3eef3 100644 --- a/libs/admin-console/src/common/collections/services/default-collection.service.ts +++ b/libs/admin-console/src/common/collections/services/default-collection.service.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { combineLatest, firstValueFrom, map, Observable, of, switchMap } from "rxjs"; +import { combineLatest, firstValueFrom, map, Observable, of, shareReplay, switchMap } from "rxjs"; import { Jsonify } from "type-fest"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; @@ -8,10 +8,10 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ActiveUserState, - StateProvider, COLLECTION_DATA, DeriveDefinition, DerivedState, + StateProvider, UserKeyDefinition, } from "@bitwarden/common/platform/state"; import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; @@ -84,6 +84,7 @@ export class DefaultCollectionService implements CollectionService { switchMap(([userId, collectionData]) => combineLatest([of(collectionData), this.keyService.orgKeys$(userId)]), ), + shareReplay({ refCount: false, bufferSize: 1 }), ); this.decryptedCollectionDataState = this.stateProvider.getDerived( From db16c98a1d93114b613398f0758c15bdbc7faf20 Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Wed, 16 Apr 2025 11:58:54 -0400 Subject: [PATCH 34/47] [PM-17773] Added "Sponsored Families" dropdown nav item in the admin console (#14029) * Added nav item for f4e in org admin console * shotgun surgery for adding "useAdminSponsoredFamilies" feature from the org table * Resolved issue with members nav item also being selected when f4e is selected * Separated out billing's logic from the org layout component * Removed unused observable * Moved logic to existing f4e policy service and added unit tests * Resolved script typescript error * Resolved goofy switchMap --------- Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> --- .../organization-layout.component.html | 26 ++- .../layouts/organization-layout.component.ts | 5 + .../members/members-routing.module.ts | 9 + .../free-families-policy.service.spec.ts | 193 ++++++++++++++++++ .../services/free-families-policy.service.ts | 46 +++++ .../models/data/organization.data.spec.ts | 1 + .../models/data/organization.data.ts | 2 + .../models/domain/organization.ts | 2 + .../response/profile-organization.response.ts | 2 + libs/common/src/enums/feature-flag.enum.ts | 2 + 10 files changed, 282 insertions(+), 6 deletions(-) create mode 100644 apps/web/src/app/billing/services/free-families-policy.service.spec.ts diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html index 47846c77571..e50c55e83d2 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html @@ -19,12 +19,26 @@ *ngIf="canShowVaultTab(organization)" > - + + + + + + + + + + + + + ; protected isBreadcrumbEventLogsEnabled$: Observable; + protected showSponsoredFamiliesDropdown$: Observable; constructor( private route: ActivatedRoute, @@ -76,6 +78,7 @@ export class OrganizationLayoutComponent implements OnInit { private providerService: ProviderService, protected bannerService: AccountDeprovisioningBannerService, private accountService: AccountService, + private freeFamiliesPolicyService: FreeFamiliesPolicyService, ) {} async ngOnInit() { @@ -92,6 +95,8 @@ export class OrganizationLayoutComponent implements OnInit { ), filter((org) => org != null), ); + this.showSponsoredFamiliesDropdown$ = + this.freeFamiliesPolicyService.showSponsoredFamiliesDropdown$(this.organization$); this.showAccountDeprovisioningBanner$ = combineLatest([ this.bannerService.showBanner$, diff --git a/apps/web/src/app/admin-console/organizations/members/members-routing.module.ts b/apps/web/src/app/admin-console/organizations/members/members-routing.module.ts index 5220ea1ef39..9666630fc08 100644 --- a/apps/web/src/app/admin-console/organizations/members/members-routing.module.ts +++ b/apps/web/src/app/admin-console/organizations/members/members-routing.module.ts @@ -3,6 +3,7 @@ import { RouterModule, Routes } from "@angular/router"; import { canAccessMembersTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { SponsoredFamiliesComponent } from "../../../billing/settings/sponsored-families.component"; import { organizationPermissionsGuard } from "../guards/org-permissions.guard"; import { MembersComponent } from "./members.component"; @@ -16,6 +17,14 @@ const routes: Routes = [ titleId: "members", }, }, + { + path: "sponsored-families", + component: SponsoredFamiliesComponent, + canActivate: [organizationPermissionsGuard(canAccessMembersTab)], + data: { + titleId: "sponsoredFamilies", + }, + }, ]; @NgModule({ diff --git a/apps/web/src/app/billing/services/free-families-policy.service.spec.ts b/apps/web/src/app/billing/services/free-families-policy.service.spec.ts new file mode 100644 index 00000000000..10ccc448986 --- /dev/null +++ b/apps/web/src/app/billing/services/free-families-policy.service.spec.ts @@ -0,0 +1,193 @@ +import { mock, MockProxy } from "jest-mock-extended"; +import { firstValueFrom, of } from "rxjs"; + +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { ProductTierType } from "@bitwarden/common/billing/enums"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; + +import { FreeFamiliesPolicyService } from "./free-families-policy.service"; + +describe("FreeFamiliesPolicyService", () => { + let service: FreeFamiliesPolicyService; + let organizationService: MockProxy; + let policyService: MockProxy; + let configService: MockProxy; + let accountService: FakeAccountService; + const userId = Utils.newGuid() as UserId; + + beforeEach(() => { + organizationService = mock(); + policyService = mock(); + configService = mock(); + accountService = mockAccountServiceWith(userId); + + service = new FreeFamiliesPolicyService( + policyService, + organizationService, + accountService, + configService, + ); + }); + + describe("showSponsoredFamiliesDropdown$", () => { + it("should return true when all conditions are met", async () => { + // Configure mocks + configService.getFeatureFlag$.mockReturnValue(of(true)); + policyService.policiesByType$.mockReturnValue(of([])); + + // Create a test organization that meets all criteria + const organization = { + id: "org-id", + productTierType: ProductTierType.Enterprise, + useAdminSponsoredFamilies: true, + isAdmin: true, + isOwner: false, + canManageUsers: false, + } as Organization; + + // Test the method + const result = await firstValueFrom(service.showSponsoredFamiliesDropdown$(of(organization))); + expect(result).toBe(true); + }); + + it("should return false when organization is not Enterprise", async () => { + // Configure mocks + configService.getFeatureFlag$.mockReturnValue(of(true)); + policyService.policiesByType$.mockReturnValue(of([])); + + // Create a test organization that is not Enterprise tier + const organization = { + id: "org-id", + productTierType: ProductTierType.Teams, + useAdminSponsoredFamilies: true, + isAdmin: true, + } as Organization; + + // Test the method + const result = await firstValueFrom(service.showSponsoredFamiliesDropdown$(of(organization))); + expect(result).toBe(false); + }); + + it("should return false when feature flag is disabled", async () => { + // Configure mocks to disable feature flag + configService.getFeatureFlag$.mockReturnValue(of(false)); + policyService.policiesByType$.mockReturnValue(of([])); + + // Create a test organization that meets other criteria + const organization = { + id: "org-id", + productTierType: ProductTierType.Enterprise, + useAdminSponsoredFamilies: true, + isAdmin: true, + } as Organization; + + // Test the method + const result = await firstValueFrom(service.showSponsoredFamiliesDropdown$(of(organization))); + expect(result).toBe(false); + }); + + it("should return false when families feature is disabled by policy", async () => { + // Configure mocks with a policy that disables the feature + configService.getFeatureFlag$.mockReturnValue(of(true)); + policyService.policiesByType$.mockReturnValue( + of([{ organizationId: "org-id", enabled: true } as Policy]), + ); + + // Create a test organization + const organization = { + id: "org-id", + productTierType: ProductTierType.Enterprise, + useAdminSponsoredFamilies: true, + isAdmin: true, + } as Organization; + + // Test the method + const result = await firstValueFrom(service.showSponsoredFamiliesDropdown$(of(organization))); + expect(result).toBe(false); + }); + + it("should return false when useAdminSponsoredFamilies is false", async () => { + // Configure mocks + configService.getFeatureFlag$.mockReturnValue(of(true)); + policyService.policiesByType$.mockReturnValue(of([])); + + // Create a test organization with useAdminSponsoredFamilies set to false + const organization = { + id: "org-id", + productTierType: ProductTierType.Enterprise, + useAdminSponsoredFamilies: false, + isAdmin: true, + } as Organization; + + // Test the method + const result = await firstValueFrom(service.showSponsoredFamiliesDropdown$(of(organization))); + expect(result).toBe(false); + }); + + it("should return true when user is an owner but not admin", async () => { + // Configure mocks + configService.getFeatureFlag$.mockReturnValue(of(true)); + policyService.policiesByType$.mockReturnValue(of([])); + + // Create a test organization where user is owner but not admin + const organization = { + id: "org-id", + productTierType: ProductTierType.Enterprise, + useAdminSponsoredFamilies: true, + isAdmin: false, + isOwner: true, + canManageUsers: false, + } as Organization; + + // Test the method + const result = await firstValueFrom(service.showSponsoredFamiliesDropdown$(of(organization))); + expect(result).toBe(true); + }); + + it("should return true when user can manage users but is not admin or owner", async () => { + // Configure mocks + configService.getFeatureFlag$.mockReturnValue(of(true)); + policyService.policiesByType$.mockReturnValue(of([])); + + // Create a test organization where user can manage users but is not admin or owner + const organization = { + id: "org-id", + productTierType: ProductTierType.Enterprise, + useAdminSponsoredFamilies: true, + isAdmin: false, + isOwner: false, + canManageUsers: true, + } as Organization; + + // Test the method + const result = await firstValueFrom(service.showSponsoredFamiliesDropdown$(of(organization))); + expect(result).toBe(true); + }); + + it("should return false when user has no admin permissions", async () => { + // Configure mocks + configService.getFeatureFlag$.mockReturnValue(of(true)); + policyService.policiesByType$.mockReturnValue(of([])); + + // Create a test organization where user has no admin permissions + const organization = { + id: "org-id", + productTierType: ProductTierType.Enterprise, + useAdminSponsoredFamilies: true, + isAdmin: false, + isOwner: false, + canManageUsers: false, + } as Organization; + + // Test the method + const result = await firstValueFrom(service.showSponsoredFamiliesDropdown$(of(organization))); + expect(result).toBe(false); + }); + }); +}); diff --git a/apps/web/src/app/billing/services/free-families-policy.service.ts b/apps/web/src/app/billing/services/free-families-policy.service.ts index 81cb970cdbe..7a8e3804b2c 100644 --- a/apps/web/src/app/billing/services/free-families-policy.service.ts +++ b/apps/web/src/app/billing/services/free-families-policy.service.ts @@ -7,6 +7,9 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { ProductTierType } from "@bitwarden/common/billing/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; interface EnterpriseOrgStatus { isFreeFamilyPolicyEnabled: boolean; @@ -26,6 +29,7 @@ export class FreeFamiliesPolicyService { private policyService: PolicyService, private organizationService: OrganizationService, private accountService: AccountService, + private configService: ConfigService, ) {} organizations$ = this.accountService.activeAccount$.pipe( @@ -42,6 +46,48 @@ export class FreeFamiliesPolicyService { return this.getFreeFamiliesVisibility$(); } + /** + * Determines whether to show the sponsored families dropdown in the organization layout + * @param organization The organization to check + * @returns Observable indicating whether to show the dropdown + */ + showSponsoredFamiliesDropdown$(organization: Observable): Observable { + const enterpriseOrganization$ = organization.pipe( + map((org) => org.productTierType === ProductTierType.Enterprise), + ); + + return this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => { + const policies$ = this.policyService.policiesByType$( + PolicyType.FreeFamiliesSponsorshipPolicy, + userId, + ); + + return combineLatest([ + enterpriseOrganization$, + this.configService.getFeatureFlag$(FeatureFlag.PM17772_AdminInitiatedSponsorships), + organization, + policies$, + ]).pipe( + map(([isEnterprise, featureFlagEnabled, org, policies]) => { + const familiesFeatureDisabled = policies.some( + (policy) => policy.organizationId === org.id && policy.enabled, + ); + + return ( + isEnterprise && + featureFlagEnabled && + !familiesFeatureDisabled && + org.useAdminSponsoredFamilies && + (org.isAdmin || org.isOwner || org.canManageUsers) + ); + }), + ); + }), + ); + } + private getFreeFamiliesVisibility$(): Observable { return combineLatest([ this.checkEnterpriseOrganizationsAndFetchPolicy(), diff --git a/libs/common/src/admin-console/models/data/organization.data.spec.ts b/libs/common/src/admin-console/models/data/organization.data.spec.ts index 5f487e1f898..fae24133502 100644 --- a/libs/common/src/admin-console/models/data/organization.data.spec.ts +++ b/libs/common/src/admin-console/models/data/organization.data.spec.ts @@ -58,6 +58,7 @@ describe("ORGANIZATIONS state", () => { familySponsorshipLastSyncDate: new Date(), userIsManagedByOrganization: false, useRiskInsights: false, + useAdminSponsoredFamilies: false, }, }; const result = sut.deserializer(JSON.parse(JSON.stringify(expectedResult))); diff --git a/libs/common/src/admin-console/models/data/organization.data.ts b/libs/common/src/admin-console/models/data/organization.data.ts index b81d06e6367..799d062aefa 100644 --- a/libs/common/src/admin-console/models/data/organization.data.ts +++ b/libs/common/src/admin-console/models/data/organization.data.ts @@ -60,6 +60,7 @@ export class OrganizationData { allowAdminAccessToAllCollectionItems: boolean; userIsManagedByOrganization: boolean; useRiskInsights: boolean; + useAdminSponsoredFamilies: boolean; constructor( response?: ProfileOrganizationResponse, @@ -122,6 +123,7 @@ export class OrganizationData { this.allowAdminAccessToAllCollectionItems = response.allowAdminAccessToAllCollectionItems; this.userIsManagedByOrganization = response.userIsManagedByOrganization; this.useRiskInsights = response.useRiskInsights; + this.useAdminSponsoredFamilies = response.useAdminSponsoredFamilies; this.isMember = options.isMember; this.isProviderUser = options.isProviderUser; diff --git a/libs/common/src/admin-console/models/domain/organization.ts b/libs/common/src/admin-console/models/domain/organization.ts index c5c5b53cce7..2e51c54b0ad 100644 --- a/libs/common/src/admin-console/models/domain/organization.ts +++ b/libs/common/src/admin-console/models/domain/organization.ts @@ -90,6 +90,7 @@ export class Organization { */ userIsManagedByOrganization: boolean; useRiskInsights: boolean; + useAdminSponsoredFamilies: boolean; constructor(obj?: OrganizationData) { if (obj == null) { @@ -148,6 +149,7 @@ export class Organization { this.allowAdminAccessToAllCollectionItems = obj.allowAdminAccessToAllCollectionItems; this.userIsManagedByOrganization = obj.userIsManagedByOrganization; this.useRiskInsights = obj.useRiskInsights; + this.useAdminSponsoredFamilies = obj.useAdminSponsoredFamilies; } get canAccess() { diff --git a/libs/common/src/admin-console/models/response/profile-organization.response.ts b/libs/common/src/admin-console/models/response/profile-organization.response.ts index 5e37cfc4c5c..da97a1034b1 100644 --- a/libs/common/src/admin-console/models/response/profile-organization.response.ts +++ b/libs/common/src/admin-console/models/response/profile-organization.response.ts @@ -55,6 +55,7 @@ export class ProfileOrganizationResponse extends BaseResponse { allowAdminAccessToAllCollectionItems: boolean; userIsManagedByOrganization: boolean; useRiskInsights: boolean; + useAdminSponsoredFamilies: boolean; constructor(response: any) { super(response); @@ -121,5 +122,6 @@ export class ProfileOrganizationResponse extends BaseResponse { ); this.userIsManagedByOrganization = this.getResponseProperty("UserIsManagedByOrganization"); this.useRiskInsights = this.getResponseProperty("UseRiskInsights"); + this.useAdminSponsoredFamilies = this.getResponseProperty("UseAdminSponsoredFamilies"); } } diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index fa776285ead..9ee1ef919f5 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -34,6 +34,7 @@ export enum FeatureFlag { PM15179_AddExistingOrgsFromProviderPortal = "pm-15179-add-existing-orgs-from-provider-portal", PM12276_BreadcrumbEventLogs = "pm-12276-breadcrumbing-for-business-features", PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method", + PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships", /* Key Management */ PrivateKeyRegeneration = "pm-12241-private-key-regeneration", @@ -117,6 +118,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.PM15179_AddExistingOrgsFromProviderPortal]: FALSE, [FeatureFlag.PM12276_BreadcrumbEventLogs]: FALSE, [FeatureFlag.PM18794_ProviderPaymentMethod]: FALSE, + [FeatureFlag.PM17772_AdminInitiatedSponsorships]: FALSE, /* Key Management */ [FeatureFlag.PrivateKeyRegeneration]: FALSE, From 6bd3fceaa1eac26fb8116fde0f308bbc40e77922 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Wed, 16 Apr 2025 17:27:48 +0100 Subject: [PATCH 35/47] fix: align upgrade badge with header text in Event Logs (#14213) --- .../organizations/manage/events.component.html | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/admin-console/organizations/manage/events.component.html b/apps/web/src/app/admin-console/organizations/manage/events.component.html index 80d22467123..2079d592a28 100644 --- a/apps/web/src/app/admin-console/organizations/manage/events.component.html +++ b/apps/web/src/app/admin-console/organizations/manage/events.component.html @@ -1,6 +1,12 @@ @let usePlaceHolderEvents = !organization?.useEvents && (isBreadcrumbEventLogsEnabled$ | async); - + {{ "upgrade" | i18n }} From 1efdcacd16d5d1fd9e0c853976c73491d40e839d Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Wed, 16 Apr 2025 13:15:43 -0400 Subject: [PATCH 36/47] [PM-16641] Remove "inline-menu-positioning-improvements" feature flag (#14225) * remove inline-menu-positioning-improvements flag * remove unused LegacyOverlayBackground * remove unused deprecated files * appease ts error TS2564 * remove deleted resources from the manifest files --- .../autofill/background/tabs.background.ts | 14 +- .../overlay.background.deprecated.ts | 124 -- .../overlay.background.deprecated.spec.ts | 1464 -------------- .../overlay.background.deprecated.ts | 811 -------- .../abstractions/autofill-init.deprecated.ts | 41 - .../content/autofill-init.deprecated.spec.ts | 604 ------ .../content/autofill-init.deprecated.ts | 315 --- .../bootstrap-legacy-autofill-overlay.ts | 14 - .../autofill-overlay-button.deprecated.ts | 29 - ...ofill-overlay-iframe.service.deprecated.ts | 33 - .../autofill-overlay-list.deprecated.ts | 31 - ...utofill-overlay-page-element.deprecated.ts | 13 - ...lay-iframe.service.deprecated.spec.ts.snap | 23 - ...l-overlay-button-iframe.deprecated.spec.ts | 26 - ...tofill-overlay-button-iframe.deprecated.ts | 21 - ...-overlay-iframe-element.deprecated.spec.ts | 46 - ...ofill-overlay-iframe-element.deprecated.ts | 22 - ...-overlay-iframe.service.deprecated.spec.ts | 521 ----- ...ofill-overlay-iframe.service.deprecated.ts | 429 ---- ...ill-overlay-list-iframe.deprecated.spec.ts | 26 - ...autofill-overlay-list-iframe.deprecated.ts | 26 - ...ill-overlay-button.deprecated.spec.ts.snap | 83 - ...autofill-overlay-button.deprecated.spec.ts | 135 -- .../autofill-overlay-button.deprecated.ts | 124 -- ...trap-autofill-overlay-button.deprecated.ts | 11 - .../overlay/pages/button/legacy-button.html | 12 - .../overlay/pages/button/legacy-button.scss | 36 - ...ofill-overlay-list.deprecated.spec.ts.snap | 537 ----- .../autofill-overlay-list.deprecated.spec.ts | 467 ----- .../list/autofill-overlay-list.deprecated.ts | 621 ------ ...tstrap-autofill-overlay-list.deprecated.ts | 11 - .../overlay/pages/list/legacy-list.html | 12 - .../overlay/pages/list/legacy-list.scss | 292 --- ...ll-overlay-page-element.deprecated.spec.ts | 222 --- ...utofill-overlay-page-element.deprecated.ts | 157 -- .../autofill-overlay-content.service.ts | 37 - ...overlay-content.service.deprecated.spec.ts | 1743 ----------------- ...fill-overlay-content.service.deprecated.ts | 1139 ----------- .../popup/settings/autofill.component.html | 10 +- .../popup/settings/autofill.component.ts | 17 +- .../src/autofill/services/autofill.service.ts | 9 - .../browser/src/background/main.background.ts | 56 +- apps/browser/src/manifest.json | 9 +- apps/browser/src/manifest.v3.json | 9 +- apps/browser/webpack.config.js | 16 - libs/common/src/enums/feature-flag.enum.ts | 2 - 46 files changed, 29 insertions(+), 10371 deletions(-) delete mode 100644 apps/browser/src/autofill/deprecated/background/abstractions/overlay.background.deprecated.ts delete mode 100644 apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts delete mode 100644 apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts delete mode 100644 apps/browser/src/autofill/deprecated/content/abstractions/autofill-init.deprecated.ts delete mode 100644 apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.spec.ts delete mode 100644 apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts delete mode 100644 apps/browser/src/autofill/deprecated/content/bootstrap-legacy-autofill-overlay.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-button.deprecated.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-iframe.service.deprecated.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-list.deprecated.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-page-element.deprecated.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/iframe-content/__snapshots__/autofill-overlay-iframe.service.deprecated.spec.ts.snap delete mode 100644 apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-button-iframe.deprecated.spec.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-button-iframe.deprecated.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe-element.deprecated.spec.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe-element.deprecated.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe.service.deprecated.spec.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe.service.deprecated.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-list-iframe.deprecated.spec.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-list-iframe.deprecated.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/button/__snapshots__/autofill-overlay-button.deprecated.spec.ts.snap delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/button/autofill-overlay-button.deprecated.spec.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/button/autofill-overlay-button.deprecated.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/button/bootstrap-autofill-overlay-button.deprecated.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/button/legacy-button.html delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/button/legacy-button.scss delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/list/__snapshots__/autofill-overlay-list.deprecated.spec.ts.snap delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/list/autofill-overlay-list.deprecated.spec.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/list/autofill-overlay-list.deprecated.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/list/bootstrap-autofill-overlay-list.deprecated.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/list/legacy-list.html delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/list/legacy-list.scss delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/shared/autofill-overlay-page-element.deprecated.spec.ts delete mode 100644 apps/browser/src/autofill/deprecated/overlay/pages/shared/autofill-overlay-page-element.deprecated.ts delete mode 100644 apps/browser/src/autofill/deprecated/services/abstractions/autofill-overlay-content.service.ts delete mode 100644 apps/browser/src/autofill/deprecated/services/autofill-overlay-content.service.deprecated.spec.ts delete mode 100644 apps/browser/src/autofill/deprecated/services/autofill-overlay-content.service.deprecated.ts diff --git a/apps/browser/src/autofill/background/tabs.background.ts b/apps/browser/src/autofill/background/tabs.background.ts index b07e06234d3..c093f1a3b00 100644 --- a/apps/browser/src/autofill/background/tabs.background.ts +++ b/apps/browser/src/autofill/background/tabs.background.ts @@ -1,7 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; - import MainBackground from "../../background/main.background"; import { OverlayBackground } from "./abstractions/overlay.background"; @@ -14,7 +10,7 @@ export default class TabsBackground { private overlayBackground: OverlayBackground, ) {} - private focusedWindowId: number; + private focusedWindowId: number = -1; /** * Initializes the window and tab listeners. @@ -90,14 +86,6 @@ export default class TabsBackground { changeInfo: chrome.tabs.TabChangeInfo, tab: chrome.tabs.Tab, ) => { - const overlayImprovementsFlag = await this.main.configService.getFeatureFlag( - FeatureFlag.InlineMenuPositioningImprovements, - ); - const removePageDetailsStatus = new Set(["loading", "unloaded"]); - if (!overlayImprovementsFlag && removePageDetailsStatus.has(changeInfo.status)) { - this.overlayBackground.removePageDetails(tabId); - } - if (this.focusedWindowId > 0 && tab.windowId !== this.focusedWindowId) { return; } diff --git a/apps/browser/src/autofill/deprecated/background/abstractions/overlay.background.deprecated.ts b/apps/browser/src/autofill/deprecated/background/abstractions/overlay.background.deprecated.ts deleted file mode 100644 index 88b78dc2495..00000000000 --- a/apps/browser/src/autofill/deprecated/background/abstractions/overlay.background.deprecated.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { CipherType } from "@bitwarden/common/vault/enums"; -import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; - -import { LockedVaultPendingNotificationsData } from "../../../background/abstractions/notification.background"; -import AutofillPageDetails from "../../../models/autofill-page-details"; - -type WebsiteIconData = { - imageEnabled: boolean; - image: string; - fallbackImage: string; - icon: string; -}; - -type OverlayAddNewItemMessage = { - login?: { - uri?: string; - hostname: string; - username: string; - password: string; - }; -}; - -type OverlayBackgroundExtensionMessage = { - [key: string]: any; - command: string; - tab?: chrome.tabs.Tab; - sender?: string; - details?: AutofillPageDetails; - overlayElement?: string; - display?: string; - data?: LockedVaultPendingNotificationsData; -} & OverlayAddNewItemMessage; - -type OverlayPortMessage = { - [key: string]: any; - command: string; - direction?: string; - overlayCipherId?: string; -}; - -type FocusedFieldData = { - focusedFieldStyles: Partial; - focusedFieldRects: Partial; - tabId?: number; -}; - -type OverlayCipherData = { - id: string; - name: string; - type: CipherType; - reprompt: CipherRepromptType; - favorite: boolean; - icon: { imageEnabled: boolean; image: string; fallbackImage: string; icon: string }; - login?: { username: string }; - card?: string; -}; - -type BackgroundMessageParam = { - message: OverlayBackgroundExtensionMessage; -}; -type BackgroundSenderParam = { - sender: chrome.runtime.MessageSender; -}; -type BackgroundOnMessageHandlerParams = BackgroundMessageParam & BackgroundSenderParam; - -type OverlayBackgroundExtensionMessageHandlers = { - [key: string]: CallableFunction; - openAutofillOverlay: () => void; - autofillOverlayElementClosed: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; - autofillOverlayAddNewVaultItem: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; - getAutofillOverlayVisibility: () => void; - checkAutofillOverlayFocused: () => void; - focusAutofillOverlayList: () => void; - updateAutofillOverlayPosition: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; - updateAutofillOverlayHidden: ({ message }: BackgroundMessageParam) => void; - updateFocusedFieldData: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; - collectPageDetailsResponse: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; - unlockCompleted: ({ message }: BackgroundMessageParam) => void; - addedCipher: () => void; - addEditCipherSubmitted: () => void; - editedCipher: () => void; - deletedCipher: () => void; -}; - -type PortMessageParam = { - message: OverlayPortMessage; -}; -type PortConnectionParam = { - port: chrome.runtime.Port; -}; -type PortOnMessageHandlerParams = PortMessageParam & PortConnectionParam; - -type OverlayButtonPortMessageHandlers = { - [key: string]: CallableFunction; - overlayButtonClicked: ({ port }: PortConnectionParam) => void; - closeAutofillOverlay: ({ port }: PortConnectionParam) => void; - forceCloseAutofillOverlay: ({ port }: PortConnectionParam) => void; - overlayPageBlurred: () => void; - redirectOverlayFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void; -}; - -type OverlayListPortMessageHandlers = { - [key: string]: CallableFunction; - checkAutofillOverlayButtonFocused: () => void; - forceCloseAutofillOverlay: ({ port }: PortConnectionParam) => void; - overlayPageBlurred: () => void; - unlockVault: ({ port }: PortConnectionParam) => void; - fillSelectedListItem: ({ message, port }: PortOnMessageHandlerParams) => void; - addNewVaultItem: ({ port }: PortConnectionParam) => void; - viewSelectedCipher: ({ message, port }: PortOnMessageHandlerParams) => void; - redirectOverlayFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void; -}; - -export { - WebsiteIconData, - OverlayBackgroundExtensionMessage, - OverlayPortMessage, - FocusedFieldData, - OverlayCipherData, - OverlayAddNewItemMessage, - OverlayBackgroundExtensionMessageHandlers, - OverlayButtonPortMessageHandlers, - OverlayListPortMessageHandlers, -}; diff --git a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts deleted file mode 100644 index 68f8032350e..00000000000 --- a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts +++ /dev/null @@ -1,1464 +0,0 @@ -import { mock, MockProxy, mockReset } from "jest-mock-extended"; -import { BehaviorSubject, of } from "rxjs"; - -import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { AuthService } from "@bitwarden/common/auth/services/auth.service"; -import { - SHOW_AUTOFILL_BUTTON, - AutofillOverlayVisibility, -} from "@bitwarden/common/autofill/constants"; -import { AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service"; -import { - DefaultDomainSettingsService, - DomainSettingsService, -} from "@bitwarden/common/autofill/services/domain-settings.service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { - EnvironmentService, - Region, -} from "@bitwarden/common/platform/abstractions/environment.service"; -import { ThemeType } from "@bitwarden/common/platform/enums"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { CloudEnvironment } from "@bitwarden/common/platform/services/default-environment.service"; -import { I18nService } from "@bitwarden/common/platform/services/i18n.service"; -import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; -import { - FakeStateProvider, - FakeAccountService, - mockAccountServiceWith, -} from "@bitwarden/common/spec"; -import { UserId } from "@bitwarden/common/types/guid"; -import { CipherType } from "@bitwarden/common/vault/enums"; -import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; - -import { BrowserApi } from "../../../platform/browser/browser-api"; -import { BrowserPlatformUtilsService } from "../../../platform/services/platform-utils/browser-platform-utils.service"; -import { - AutofillOverlayElement, - AutofillOverlayPort, - RedirectFocusDirection, -} from "../../enums/autofill-overlay.enum"; -import { AutofillService } from "../../services/abstractions/autofill.service"; -import { - createAutofillPageDetailsMock, - createChromeTabMock, - createFocusedFieldDataMock, - createPageDetailMock, - createPortSpyMock, -} from "../../spec/autofill-mocks"; -import { flushPromises, sendMockExtensionMessage, sendPortMessage } from "../../spec/testing-utils"; - -import LegacyOverlayBackground from "./overlay.background.deprecated"; - -describe("OverlayBackground", () => { - const mockUserId = Utils.newGuid() as UserId; - const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); - const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService); - let domainSettingsService: DomainSettingsService; - let buttonPortSpy: chrome.runtime.Port; - let listPortSpy: chrome.runtime.Port; - let overlayBackground: LegacyOverlayBackground; - const cipherService = mock(); - const autofillService = mock(); - let configService: MockProxy; - let activeAccountStatusMock$: BehaviorSubject; - let authService: MockProxy; - - const environmentService = mock(); - environmentService.environment$ = new BehaviorSubject( - new CloudEnvironment({ - key: Region.US, - domain: "bitwarden.com", - urls: { icons: "https://icons.bitwarden.com/" }, - }), - ); - const autofillSettingsService = mock(); - const i18nService = mock(); - const platformUtilsService = mock(); - const themeStateService = mock(); - const initOverlayElementPorts = async (options = { initList: true, initButton: true }) => { - const { initList, initButton } = options; - if (initButton) { - await overlayBackground["handlePortOnConnect"](createPortSpyMock(AutofillOverlayPort.Button)); - buttonPortSpy = overlayBackground["overlayButtonPort"]; - } - - if (initList) { - await overlayBackground["handlePortOnConnect"](createPortSpyMock(AutofillOverlayPort.List)); - listPortSpy = overlayBackground["overlayListPort"]; - } - - return { buttonPortSpy, listPortSpy }; - }; - - beforeEach(() => { - configService = mock(); - configService.getFeatureFlag$.mockImplementation(() => of(true)); - domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider, configService); - activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Unlocked); - authService = mock(); - authService.activeAccountStatus$ = activeAccountStatusMock$; - overlayBackground = new LegacyOverlayBackground( - cipherService, - autofillService, - authService, - environmentService, - domainSettingsService, - autofillSettingsService, - i18nService, - platformUtilsService, - themeStateService, - accountService, - ); - - jest - .spyOn(overlayBackground as any, "getOverlayVisibility") - .mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus); - - themeStateService.selectedTheme$ = of(ThemeType.Light); - domainSettingsService.showFavicons$ = of(true); - - void overlayBackground.init(); - }); - - afterEach(() => { - jest.clearAllMocks(); - mockReset(cipherService); - }); - - describe("removePageDetails", () => { - it("removes the page details for a specific tab from the pageDetailsForTab object", () => { - const tabId = 1; - const frameId = 2; - overlayBackground["pageDetailsForTab"][tabId] = new Map([[frameId, createPageDetailMock()]]); - overlayBackground.removePageDetails(tabId); - - expect(overlayBackground["pageDetailsForTab"][tabId]).toBeUndefined(); - }); - }); - - describe("init", () => { - it("sets up the extension message listeners, get the overlay's visibility settings, and get the user's auth status", async () => { - overlayBackground["setupExtensionMessageListeners"] = jest.fn(); - overlayBackground["getOverlayVisibility"] = jest.fn(); - overlayBackground["getAuthStatus"] = jest.fn(); - - await overlayBackground.init(); - - expect(overlayBackground["setupExtensionMessageListeners"]).toHaveBeenCalled(); - expect(overlayBackground["getOverlayVisibility"]).toHaveBeenCalled(); - expect(overlayBackground["getAuthStatus"]).toHaveBeenCalled(); - }); - }); - - describe("updateOverlayCiphers", () => { - const url = "https://jest-testing-website.com"; - const tab = createChromeTabMock({ url }); - const cipher1 = mock({ - id: "id-1", - localData: { lastUsedDate: 222 }, - name: "name-1", - type: CipherType.Login, - login: { username: "username-1", uri: url }, - }); - const cipher2 = mock({ - id: "id-2", - localData: { lastUsedDate: 111 }, - name: "name-2", - type: CipherType.Login, - login: { username: "username-2", uri: url }, - }); - - beforeEach(() => { - activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); - }); - - it("ignores updating the overlay ciphers if the user's auth status is not unlocked", async () => { - activeAccountStatusMock$.next(AuthenticationStatus.Locked); - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId"); - jest.spyOn(cipherService, "getAllDecryptedForUrl"); - - await overlayBackground.updateOverlayCiphers(); - - expect(BrowserApi.getTabFromCurrentWindowId).not.toHaveBeenCalled(); - expect(cipherService.getAllDecryptedForUrl).not.toHaveBeenCalled(); - }); - - it("ignores updating the overlay ciphers if the tab is undefined", async () => { - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(undefined); - jest.spyOn(cipherService, "getAllDecryptedForUrl"); - - await overlayBackground.updateOverlayCiphers(); - - expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled(); - expect(cipherService.getAllDecryptedForUrl).not.toHaveBeenCalled(); - }); - - it("queries all ciphers for the given url, sort them by last used, and format them for usage in the overlay", async () => { - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(tab); - cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]); - cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); - jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); - jest.spyOn(overlayBackground as any, "getOverlayCipherData"); - - await overlayBackground.updateOverlayCiphers(); - - expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled(); - expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url, mockUserId); - expect(overlayBackground["cipherService"].sortCiphersByLastUsedThenName).toHaveBeenCalled(); - expect(overlayBackground["overlayLoginCiphers"]).toStrictEqual( - new Map([ - ["overlay-cipher-0", cipher2], - ["overlay-cipher-1", cipher1], - ]), - ); - expect(overlayBackground["getOverlayCipherData"]).toHaveBeenCalled(); - }); - - it("posts an `updateOverlayListCiphers` message to the overlay list port, and send a `updateIsOverlayCiphersPopulated` message to the tab indicating that the list of ciphers is populated", async () => { - overlayBackground["overlayListPort"] = mock(); - cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]); - cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(tab); - jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); - - await overlayBackground.updateOverlayCiphers(); - - expect(overlayBackground["overlayListPort"].postMessage).toHaveBeenCalledWith({ - command: "updateOverlayListCiphers", - ciphers: [ - { - card: null, - favorite: cipher2.favorite, - icon: { - fallbackImage: "images/bwi-globe.png", - icon: "bwi-globe", - image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", - imageEnabled: true, - }, - id: "overlay-cipher-0", - login: { - username: "username-2", - }, - name: "name-2", - reprompt: cipher2.reprompt, - type: 1, - }, - { - card: null, - favorite: cipher1.favorite, - icon: { - fallbackImage: "images/bwi-globe.png", - icon: "bwi-globe", - image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", - imageEnabled: true, - }, - id: "overlay-cipher-1", - login: { - username: "username-1", - }, - name: "name-1", - reprompt: cipher1.reprompt, - type: 1, - }, - ], - }); - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - tab, - "updateIsOverlayCiphersPopulated", - { isOverlayCiphersPopulated: true }, - ); - }); - }); - - describe("getOverlayCipherData", () => { - const url = "https://jest-testing-website.com"; - const cipher1 = mock({ - id: "id-1", - localData: { lastUsedDate: 222 }, - name: "name-1", - type: CipherType.Login, - login: { username: "username-1", uri: url }, - }); - const cipher2 = mock({ - id: "id-2", - localData: { lastUsedDate: 111 }, - name: "name-2", - type: CipherType.Login, - login: { username: "username-2", uri: url }, - }); - const cipher3 = mock({ - id: "id-3", - localData: { lastUsedDate: 333 }, - name: "name-3", - type: CipherType.Card, - card: { subTitle: "Visa, *6789" }, - }); - const cipher4 = mock({ - id: "id-4", - localData: { lastUsedDate: 444 }, - name: "name-4", - type: CipherType.Card, - card: { subTitle: "Mastercard, *1234" }, - }); - - it("formats and returns the cipher data", async () => { - overlayBackground["overlayLoginCiphers"] = new Map([ - ["overlay-cipher-0", cipher2], - ["overlay-cipher-1", cipher1], - ["overlay-cipher-2", cipher3], - ["overlay-cipher-3", cipher4], - ]); - - const overlayCipherData = await overlayBackground["getOverlayCipherData"](); - - expect(overlayCipherData).toStrictEqual([ - { - card: null, - favorite: cipher2.favorite, - icon: { - fallbackImage: "images/bwi-globe.png", - icon: "bwi-globe", - image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", - imageEnabled: true, - }, - id: "overlay-cipher-0", - login: { - username: "username-2", - }, - name: "name-2", - reprompt: cipher2.reprompt, - type: 1, - }, - { - card: null, - favorite: cipher1.favorite, - icon: { - fallbackImage: "images/bwi-globe.png", - icon: "bwi-globe", - image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", - imageEnabled: true, - }, - id: "overlay-cipher-1", - login: { - username: "username-1", - }, - name: "name-1", - reprompt: cipher1.reprompt, - type: 1, - }, - { - card: "Visa, *6789", - favorite: cipher3.favorite, - icon: { - fallbackImage: "", - icon: "bwi-credit-card", - image: null, - imageEnabled: true, - }, - id: "overlay-cipher-2", - login: null, - name: "name-3", - reprompt: cipher3.reprompt, - type: 3, - }, - { - card: "Mastercard, *1234", - favorite: cipher4.favorite, - icon: { - fallbackImage: "", - icon: "bwi-credit-card", - image: null, - imageEnabled: true, - }, - id: "overlay-cipher-3", - login: null, - name: "name-4", - reprompt: cipher4.reprompt, - type: 3, - }, - ]); - }); - }); - - describe("getAuthStatus", () => { - it("will update the user's auth status but will not update the overlay ciphers", async () => { - const authStatus = AuthenticationStatus.Unlocked; - overlayBackground["userAuthStatus"] = AuthenticationStatus.Unlocked; - jest.spyOn(overlayBackground["authService"], "getAuthStatus").mockResolvedValue(authStatus); - jest.spyOn(overlayBackground as any, "updateOverlayButtonAuthStatus").mockImplementation(); - jest.spyOn(overlayBackground as any, "updateOverlayCiphers").mockImplementation(); - - const status = await overlayBackground["getAuthStatus"](); - - expect(overlayBackground["authService"].getAuthStatus).toHaveBeenCalled(); - expect(overlayBackground["updateOverlayButtonAuthStatus"]).not.toHaveBeenCalled(); - expect(overlayBackground["updateOverlayCiphers"]).not.toHaveBeenCalled(); - expect(overlayBackground["userAuthStatus"]).toBe(authStatus); - expect(status).toBe(authStatus); - }); - - it("will update the user's auth status and update the overlay ciphers if the status has been modified", async () => { - const authStatus = AuthenticationStatus.Unlocked; - overlayBackground["userAuthStatus"] = AuthenticationStatus.LoggedOut; - jest.spyOn(overlayBackground["authService"], "getAuthStatus").mockResolvedValue(authStatus); - jest.spyOn(overlayBackground as any, "updateOverlayButtonAuthStatus").mockImplementation(); - jest.spyOn(overlayBackground as any, "updateOverlayCiphers").mockImplementation(); - - await overlayBackground["getAuthStatus"](); - - expect(overlayBackground["authService"].getAuthStatus).toHaveBeenCalled(); - expect(overlayBackground["updateOverlayButtonAuthStatus"]).toHaveBeenCalled(); - expect(overlayBackground["updateOverlayCiphers"]).toHaveBeenCalled(); - expect(overlayBackground["userAuthStatus"]).toBe(authStatus); - }); - }); - - describe("updateOverlayButtonAuthStatus", () => { - it("will send a message to the button port with the user's auth status", () => { - overlayBackground["overlayButtonPort"] = mock(); - jest.spyOn(overlayBackground["overlayButtonPort"], "postMessage"); - - overlayBackground["updateOverlayButtonAuthStatus"](); - - expect(overlayBackground["overlayButtonPort"].postMessage).toHaveBeenCalledWith({ - command: "updateOverlayButtonAuthStatus", - authStatus: overlayBackground["userAuthStatus"], - }); - }); - }); - - describe("getTranslations", () => { - it("will query the overlay page translations if they have not been queried", () => { - overlayBackground["overlayPageTranslations"] = undefined; - jest.spyOn(overlayBackground as any, "getTranslations"); - jest.spyOn(overlayBackground["i18nService"], "translate").mockImplementation((key) => key); - jest.spyOn(BrowserApi, "getUILanguage").mockReturnValue("en"); - - const translations = overlayBackground["getTranslations"](); - - expect(overlayBackground["getTranslations"]).toHaveBeenCalled(); - const translationKeys = [ - "opensInANewWindow", - "bitwardenOverlayButton", - "toggleBitwardenVaultOverlay", - "bitwardenVault", - "unlockYourAccountToViewMatchingLogins", - "unlockAccount", - "fillCredentialsFor", - "partialUsername", - "view", - "noItemsToShow", - "newItem", - "addNewVaultItem", - ]; - translationKeys.forEach((key) => { - expect(overlayBackground["i18nService"].translate).toHaveBeenCalledWith(key); - }); - expect(translations).toStrictEqual({ - locale: "en", - opensInANewWindow: "opensInANewWindow", - buttonPageTitle: "bitwardenOverlayButton", - toggleBitwardenVaultOverlay: "toggleBitwardenVaultOverlay", - listPageTitle: "bitwardenVault", - unlockYourAccount: "unlockYourAccountToViewMatchingLogins", - unlockAccount: "unlockAccount", - fillCredentialsFor: "fillCredentialsFor", - partialUsername: "partialUsername", - view: "view", - noItemsToShow: "noItemsToShow", - newItem: "newItem", - addNewVaultItem: "addNewVaultItem", - }); - }); - }); - - describe("setupExtensionMessageListeners", () => { - it("will set up onMessage and onConnect listeners", () => { - overlayBackground["setupExtensionMessageListeners"](); - - expect(chrome.runtime.onMessage.addListener).toHaveBeenCalled(); - expect(chrome.runtime.onConnect.addListener).toHaveBeenCalled(); - }); - }); - - describe("handleExtensionMessage", () => { - it("will return early if the message command is not present within the extensionMessageHandlers", () => { - const message = { - command: "not-a-command", - }; - const sender = mock({ tab: { id: 1 } }); - const sendResponse = jest.fn(); - - const returnValue = overlayBackground["handleExtensionMessage"]( - message, - sender, - sendResponse, - ); - - expect(returnValue).toBe(null); - expect(sendResponse).not.toHaveBeenCalled(); - }); - - it("will trigger the message handler and return undefined if the message does not have a response", () => { - const message = { - command: "autofillOverlayElementClosed", - }; - const sender = mock({ tab: { id: 1 } }); - const sendResponse = jest.fn(); - jest.spyOn(overlayBackground as any, "overlayElementClosed"); - - const returnValue = overlayBackground["handleExtensionMessage"]( - message, - sender, - sendResponse, - ); - - expect(returnValue).toBe(null); - expect(sendResponse).not.toHaveBeenCalled(); - expect(overlayBackground["overlayElementClosed"]).toHaveBeenCalledWith(message, sender); - }); - - it("will return a response if the message handler returns a response", async () => { - const message = { - command: "openAutofillOverlay", - }; - const sender = mock({ tab: { id: 1 } }); - const sendResponse = jest.fn(); - jest.spyOn(overlayBackground as any, "getTranslations").mockReturnValue("translations"); - - const returnValue = overlayBackground["handleExtensionMessage"]( - message, - sender, - sendResponse, - ); - - expect(returnValue).toBe(true); - }); - - describe("extension message handlers", () => { - beforeEach(() => { - jest - .spyOn(overlayBackground as any, "getAuthStatus") - .mockResolvedValue(AuthenticationStatus.Unlocked); - }); - - describe("openAutofillOverlay message handler", () => { - it("opens the autofill overlay by sending a message to the current tab", async () => { - const sender = mock({ tab: { id: 1 } }); - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(sender.tab); - jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); - - sendMockExtensionMessage({ command: "openAutofillOverlay" }); - await flushPromises(); - - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - sender.tab, - "openAutofillOverlay", - { - isFocusingFieldElement: false, - isOpeningFullOverlay: false, - authStatus: AuthenticationStatus.Unlocked, - }, - ); - }); - }); - - describe("autofillOverlayElementClosed message handler", () => { - beforeEach(async () => { - await initOverlayElementPorts(); - }); - - it("disconnects any expired ports if the sender is not from the same page as the most recently focused field", () => { - const port1 = mock(); - const port2 = mock(); - overlayBackground["expiredPorts"] = [port1, port2]; - const sender = mock({ tab: { id: 1 } }); - const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage( - { - command: "autofillOverlayElementClosed", - overlayElement: AutofillOverlayElement.Button, - }, - sender, - ); - - expect(port1.disconnect).toHaveBeenCalled(); - expect(port2.disconnect).toHaveBeenCalled(); - }); - - it("disconnects the button element port", () => { - sendMockExtensionMessage({ - command: "autofillOverlayElementClosed", - overlayElement: AutofillOverlayElement.Button, - }); - - expect(buttonPortSpy.disconnect).toHaveBeenCalled(); - expect(overlayBackground["overlayButtonPort"]).toBeNull(); - }); - - it("disconnects the list element port", () => { - sendMockExtensionMessage({ - command: "autofillOverlayElementClosed", - overlayElement: AutofillOverlayElement.List, - }); - - expect(listPortSpy.disconnect).toHaveBeenCalled(); - expect(overlayBackground["overlayListPort"]).toBeNull(); - }); - }); - - describe("autofillOverlayAddNewVaultItem message handler", () => { - let sender: chrome.runtime.MessageSender; - beforeEach(() => { - sender = mock({ tab: { id: 1 } }); - jest - .spyOn(overlayBackground["cipherService"], "setAddEditCipherInfo") - .mockImplementation(); - jest.spyOn(overlayBackground as any, "openAddEditVaultItemPopout").mockImplementation(); - }); - - it("will not open the add edit popout window if the message does not have a login cipher provided", () => { - sendMockExtensionMessage({ command: "autofillOverlayAddNewVaultItem" }, sender); - - expect(overlayBackground["cipherService"].setAddEditCipherInfo).not.toHaveBeenCalled(); - expect(overlayBackground["openAddEditVaultItemPopout"]).not.toHaveBeenCalled(); - }); - - it("will open the add edit popout window after creating a new cipher", async () => { - jest.spyOn(BrowserApi, "sendMessage"); - - sendMockExtensionMessage( - { - command: "autofillOverlayAddNewVaultItem", - login: { - uri: "https://tacos.com", - hostname: "", - username: "username", - password: "password", - }, - }, - sender, - ); - await flushPromises(); - - expect(overlayBackground["cipherService"].setAddEditCipherInfo).toHaveBeenCalled(); - expect(overlayBackground["openAddEditVaultItemPopout"]).toHaveBeenCalled(); - }); - }); - - describe("getAutofillOverlayVisibility message handler", () => { - beforeEach(() => { - jest - .spyOn(overlayBackground as any, "getOverlayVisibility") - .mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus); - }); - - it("will set the overlayVisibility property", async () => { - sendMockExtensionMessage({ command: "getAutofillOverlayVisibility" }); - await flushPromises(); - - expect(await overlayBackground["getOverlayVisibility"]()).toBe( - AutofillOverlayVisibility.OnFieldFocus, - ); - }); - - it("returns the overlayVisibility property", async () => { - const sendMessageSpy = jest.fn(); - - sendMockExtensionMessage( - { command: "getAutofillOverlayVisibility" }, - undefined, - sendMessageSpy, - ); - await flushPromises(); - - expect(sendMessageSpy).toHaveBeenCalledWith(AutofillOverlayVisibility.OnFieldFocus); - }); - }); - - describe("checkAutofillOverlayFocused message handler", () => { - beforeEach(async () => { - await initOverlayElementPorts(); - }); - - it("will check if the overlay list is focused if the list port is open", () => { - sendMockExtensionMessage({ command: "checkAutofillOverlayFocused" }); - - expect(listPortSpy.postMessage).toHaveBeenCalledWith({ - command: "checkAutofillOverlayListFocused", - }); - expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "checkAutofillOverlayButtonFocused", - }); - }); - - it("will check if the overlay button is focused if the list port is not open", () => { - overlayBackground["overlayListPort"] = undefined; - - sendMockExtensionMessage({ command: "checkAutofillOverlayFocused" }); - - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "checkAutofillOverlayButtonFocused", - }); - expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "checkAutofillOverlayListFocused", - }); - }); - }); - - describe("focusAutofillOverlayList message handler", () => { - it("will send a `focusOverlayList` message to the overlay list port", async () => { - await initOverlayElementPorts({ initList: true, initButton: false }); - - sendMockExtensionMessage({ command: "focusAutofillOverlayList" }); - - expect(listPortSpy.postMessage).toHaveBeenCalledWith({ command: "focusOverlayList" }); - }); - }); - - describe("updateAutofillOverlayPosition message handler", () => { - beforeEach(async () => { - await overlayBackground["handlePortOnConnect"]( - createPortSpyMock(AutofillOverlayPort.List), - ); - listPortSpy = overlayBackground["overlayListPort"]; - - await overlayBackground["handlePortOnConnect"]( - createPortSpyMock(AutofillOverlayPort.Button), - ); - buttonPortSpy = overlayBackground["overlayButtonPort"]; - }); - - it("ignores updating the position if the overlay element type is not provided", () => { - sendMockExtensionMessage({ command: "updateAutofillOverlayPosition" }); - - expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: expect.anything(), - }); - expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: expect.anything(), - }); - }); - - it("skips updating the position if the most recently focused field is different than the message sender", () => { - const sender = mock({ tab: { id: 1 } }); - const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ command: "updateAutofillOverlayPosition" }, sender); - - expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: expect.anything(), - }); - expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: expect.anything(), - }); - }); - - it("updates the overlay button's position", () => { - const focusedFieldData = createFocusedFieldDataMock(); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ - command: "updateAutofillOverlayPosition", - overlayElement: AutofillOverlayElement.Button, - }); - - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: { height: "2px", left: "4px", top: "2px", width: "2px" }, - }); - }); - - it("modifies the overlay button's height for medium sized input elements", () => { - const focusedFieldData = createFocusedFieldDataMock({ - focusedFieldRects: { top: 1, left: 2, height: 35, width: 4 }, - }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ - command: "updateAutofillOverlayPosition", - overlayElement: AutofillOverlayElement.Button, - }); - - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: { height: "20px", left: "-22px", top: "8px", width: "20px" }, - }); - }); - - it("modifies the overlay button's height for large sized input elements", () => { - const focusedFieldData = createFocusedFieldDataMock({ - focusedFieldRects: { top: 1, left: 2, height: 50, width: 4 }, - }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ - command: "updateAutofillOverlayPosition", - overlayElement: AutofillOverlayElement.Button, - }); - - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: { height: "27px", left: "-32px", top: "13px", width: "27px" }, - }); - }); - - it("takes into account the right padding of the focused field in positioning the button if the right padding of the field is larger than the left padding", () => { - const focusedFieldData = createFocusedFieldDataMock({ - focusedFieldStyles: { paddingRight: "20px", paddingLeft: "6px" }, - }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ - command: "updateAutofillOverlayPosition", - overlayElement: AutofillOverlayElement.Button, - }); - - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: { height: "2px", left: "-18px", top: "2px", width: "2px" }, - }); - }); - - it("will post a message to the overlay list facilitating an update of the list's position", () => { - const sender = mock({ tab: { id: 1 } }); - const focusedFieldData = createFocusedFieldDataMock(); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - overlayBackground["updateOverlayPosition"]( - { overlayElement: AutofillOverlayElement.List }, - sender, - ); - sendMockExtensionMessage({ - command: "updateAutofillOverlayPosition", - overlayElement: AutofillOverlayElement.List, - }); - - expect(listPortSpy.postMessage).toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: { left: "2px", top: "4px", width: "4px" }, - }); - }); - }); - - describe("updateOverlayHidden", () => { - beforeEach(async () => { - await initOverlayElementPorts(); - }); - - it("returns early if the display value is not provided", () => { - const message = { - command: "updateAutofillOverlayHidden", - }; - - sendMockExtensionMessage(message); - - expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith(message); - expect(listPortSpy.postMessage).not.toHaveBeenCalledWith(message); - }); - - it("posts a message to the overlay button and list with the display value", () => { - const message = { command: "updateAutofillOverlayHidden", display: "none" }; - - sendMockExtensionMessage(message); - - expect(overlayBackground["overlayButtonPort"].postMessage).toHaveBeenCalledWith({ - command: "updateOverlayHidden", - styles: { - display: message.display, - }, - }); - expect(overlayBackground["overlayListPort"].postMessage).toHaveBeenCalledWith({ - command: "updateOverlayHidden", - styles: { - display: message.display, - }, - }); - }); - }); - - describe("collectPageDetailsResponse message handler", () => { - let sender: chrome.runtime.MessageSender; - const pageDetails1 = createAutofillPageDetailsMock({ - login: { username: "username1", password: "password1" }, - }); - const pageDetails2 = createAutofillPageDetailsMock({ - login: { username: "username2", password: "password2" }, - }); - - beforeEach(() => { - sender = mock({ tab: { id: 1 } }); - }); - - it("stores the page details provided by the message by the tab id of the sender", () => { - sendMockExtensionMessage( - { command: "collectPageDetailsResponse", details: pageDetails1 }, - sender, - ); - - expect(overlayBackground["pageDetailsForTab"][sender.tab.id]).toStrictEqual( - new Map([ - [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails1 }], - ]), - ); - }); - - it("updates the page details for a tab that already has a set of page details stored ", () => { - const secondFrameSender = mock({ - tab: { id: 1 }, - frameId: 3, - }); - overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ - [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails1 }], - ]); - - sendMockExtensionMessage( - { command: "collectPageDetailsResponse", details: pageDetails2 }, - secondFrameSender, - ); - - expect(overlayBackground["pageDetailsForTab"][sender.tab.id]).toStrictEqual( - new Map([ - [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails1 }], - [ - secondFrameSender.frameId, - { - frameId: secondFrameSender.frameId, - tab: secondFrameSender.tab, - details: pageDetails2, - }, - ], - ]), - ); - }); - }); - - describe("unlockCompleted message handler", () => { - let getAuthStatusSpy: jest.SpyInstance; - - beforeEach(() => { - overlayBackground["userAuthStatus"] = AuthenticationStatus.LoggedOut; - jest.spyOn(BrowserApi, "tabSendMessageData"); - getAuthStatusSpy = jest - .spyOn(overlayBackground as any, "getAuthStatus") - .mockImplementation(() => { - overlayBackground["userAuthStatus"] = AuthenticationStatus.Unlocked; - return Promise.resolve(AuthenticationStatus.Unlocked); - }); - }); - - it("updates the user's auth status but does not open the overlay", async () => { - const message = { - command: "unlockCompleted", - data: { - commandToRetry: { message: { command: "" } }, - }, - }; - - sendMockExtensionMessage(message); - await flushPromises(); - - expect(getAuthStatusSpy).toHaveBeenCalled(); - expect(BrowserApi.tabSendMessageData).not.toHaveBeenCalled(); - }); - - it("updates user's auth status and opens the overlay if a follow up command is provided", async () => { - const sender = mock({ tab: { id: 1 } }); - const message = { - command: "unlockCompleted", - data: { - commandToRetry: { message: { command: "openAutofillOverlay" } }, - }, - }; - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(sender.tab); - - sendMockExtensionMessage(message); - await flushPromises(); - - expect(getAuthStatusSpy).toHaveBeenCalled(); - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - sender.tab, - "openAutofillOverlay", - { - isFocusingFieldElement: true, - isOpeningFullOverlay: false, - authStatus: AuthenticationStatus.Unlocked, - }, - ); - }); - }); - - describe("extension messages that trigger an update of the inline menu ciphers", () => { - const extensionMessages = [ - "addedCipher", - "addEditCipherSubmitted", - "editedCipher", - "deletedCipher", - ]; - - beforeEach(() => { - jest.spyOn(overlayBackground, "updateOverlayCiphers").mockImplementation(); - }); - - extensionMessages.forEach((message) => { - it(`triggers an update of the overlay ciphers when the ${message} message is received`, () => { - sendMockExtensionMessage({ command: message }); - expect(overlayBackground.updateOverlayCiphers).toHaveBeenCalled(); - }); - }); - }); - }); - }); - - describe("handlePortOnConnect", () => { - beforeEach(() => { - jest.spyOn(overlayBackground as any, "updateOverlayPosition").mockImplementation(); - jest.spyOn(overlayBackground as any, "getAuthStatus").mockImplementation(); - jest.spyOn(overlayBackground as any, "getTranslations").mockImplementation(); - jest.spyOn(overlayBackground as any, "getOverlayCipherData").mockImplementation(); - }); - - it("skips setting up the overlay port if the port connection is not for an overlay element", async () => { - const port = createPortSpyMock("not-an-overlay-element"); - - await overlayBackground["handlePortOnConnect"](port); - - expect(port.onMessage.addListener).not.toHaveBeenCalled(); - expect(port.postMessage).not.toHaveBeenCalled(); - }); - - it("sets up the overlay list port if the port connection is for the overlay list", async () => { - await initOverlayElementPorts({ initList: true, initButton: false }); - await flushPromises(); - - expect(overlayBackground["overlayButtonPort"]).toBeUndefined(); - expect(listPortSpy.onMessage.addListener).toHaveBeenCalled(); - expect(listPortSpy.postMessage).toHaveBeenCalled(); - expect(overlayBackground["getAuthStatus"]).toHaveBeenCalled(); - expect(chrome.runtime.getURL).toHaveBeenCalledWith("overlay/list.css"); - expect(overlayBackground["getTranslations"]).toHaveBeenCalled(); - expect(overlayBackground["getOverlayCipherData"]).toHaveBeenCalled(); - expect(overlayBackground["updateOverlayPosition"]).toHaveBeenCalledWith( - { overlayElement: AutofillOverlayElement.List }, - listPortSpy.sender, - ); - }); - - it("sets up the overlay button port if the port connection is for the overlay button", async () => { - await initOverlayElementPorts({ initList: false, initButton: true }); - await flushPromises(); - - expect(overlayBackground["overlayListPort"]).toBeUndefined(); - expect(buttonPortSpy.onMessage.addListener).toHaveBeenCalled(); - expect(buttonPortSpy.postMessage).toHaveBeenCalled(); - expect(overlayBackground["getAuthStatus"]).toHaveBeenCalled(); - expect(chrome.runtime.getURL).toHaveBeenCalledWith("overlay/button.css"); - expect(overlayBackground["getTranslations"]).toHaveBeenCalled(); - expect(overlayBackground["updateOverlayPosition"]).toHaveBeenCalledWith( - { overlayElement: AutofillOverlayElement.Button }, - buttonPortSpy.sender, - ); - }); - - it("stores an existing overlay port so that it can be disconnected at a later time", async () => { - overlayBackground["overlayButtonPort"] = mock(); - - await initOverlayElementPorts({ initList: false, initButton: true }); - await flushPromises(); - - expect(overlayBackground["expiredPorts"].length).toBe(1); - }); - - it("gets the system theme", async () => { - themeStateService.selectedTheme$ = of(ThemeType.System); - - await initOverlayElementPorts({ initList: true, initButton: false }); - await flushPromises(); - - expect(listPortSpy.postMessage).toHaveBeenCalledWith( - expect.objectContaining({ theme: ThemeType.System }), - ); - }); - }); - - describe("handleOverlayElementPortMessage", () => { - beforeEach(async () => { - await initOverlayElementPorts(); - overlayBackground["userAuthStatus"] = AuthenticationStatus.Unlocked; - }); - - it("ignores port messages that do not contain a handler", () => { - jest.spyOn(overlayBackground as any, "checkOverlayButtonFocused").mockImplementation(); - - sendPortMessage(buttonPortSpy, { command: "checkAutofillOverlayButtonFocused" }); - - expect(overlayBackground["checkOverlayButtonFocused"]).not.toHaveBeenCalled(); - }); - - describe("overlay button message handlers", () => { - it("unlocks the vault if the user auth status is not unlocked", () => { - overlayBackground["userAuthStatus"] = AuthenticationStatus.LoggedOut; - jest.spyOn(overlayBackground as any, "unlockVault").mockImplementation(); - - sendPortMessage(buttonPortSpy, { command: "overlayButtonClicked" }); - - expect(overlayBackground["unlockVault"]).toHaveBeenCalled(); - }); - - it("opens the autofill overlay if the auth status is unlocked", () => { - jest.spyOn(overlayBackground as any, "openOverlay").mockImplementation(); - - sendPortMessage(buttonPortSpy, { command: "overlayButtonClicked" }); - - expect(overlayBackground["openOverlay"]).toHaveBeenCalled(); - }); - - describe("closeAutofillOverlay", () => { - it("sends a `closeOverlay` message to the sender tab", () => { - jest.spyOn(BrowserApi, "tabSendMessageData"); - - sendPortMessage(buttonPortSpy, { command: "closeAutofillOverlay" }); - - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - buttonPortSpy.sender.tab, - "closeAutofillOverlay", - { forceCloseOverlay: false }, - ); - }); - }); - - describe("forceCloseAutofillOverlay", () => { - it("sends a `closeOverlay` message to the sender tab with a `forceCloseOverlay` flag of `true` set", () => { - jest.spyOn(BrowserApi, "tabSendMessageData"); - - sendPortMessage(buttonPortSpy, { command: "forceCloseAutofillOverlay" }); - - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - buttonPortSpy.sender.tab, - "closeAutofillOverlay", - { forceCloseOverlay: true }, - ); - }); - }); - - describe("overlayPageBlurred", () => { - it("checks if the overlay list is focused", () => { - jest.spyOn(overlayBackground as any, "checkOverlayListFocused"); - - sendPortMessage(buttonPortSpy, { command: "overlayPageBlurred" }); - - expect(overlayBackground["checkOverlayListFocused"]).toHaveBeenCalled(); - }); - }); - - describe("redirectOverlayFocusOut", () => { - beforeEach(() => { - jest.spyOn(BrowserApi, "tabSendMessageData"); - }); - - it("ignores the redirect message if the direction is not provided", () => { - sendPortMessage(buttonPortSpy, { command: "redirectOverlayFocusOut" }); - - expect(BrowserApi.tabSendMessageData).not.toHaveBeenCalled(); - }); - - it("sends the redirect message if the direction is provided", () => { - sendPortMessage(buttonPortSpy, { - command: "redirectOverlayFocusOut", - direction: RedirectFocusDirection.Next, - }); - - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - buttonPortSpy.sender.tab, - "redirectOverlayFocusOut", - { direction: RedirectFocusDirection.Next }, - ); - }); - }); - }); - - describe("overlay list message handlers", () => { - describe("checkAutofillOverlayButtonFocused", () => { - it("checks on the focus state of the overlay button", () => { - jest.spyOn(overlayBackground as any, "checkOverlayButtonFocused").mockImplementation(); - - sendPortMessage(listPortSpy, { command: "checkAutofillOverlayButtonFocused" }); - - expect(overlayBackground["checkOverlayButtonFocused"]).toHaveBeenCalled(); - }); - }); - - describe("forceCloseAutofillOverlay", () => { - it("sends a `closeOverlay` message to the sender tab with a `forceCloseOverlay` flag of `true` set", () => { - jest.spyOn(BrowserApi, "tabSendMessageData"); - - sendPortMessage(listPortSpy, { command: "forceCloseAutofillOverlay" }); - - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - listPortSpy.sender.tab, - "closeAutofillOverlay", - { forceCloseOverlay: true }, - ); - }); - }); - - describe("overlayPageBlurred", () => { - it("checks on the focus state of the overlay button", () => { - jest.spyOn(overlayBackground as any, "checkOverlayButtonFocused").mockImplementation(); - - sendPortMessage(listPortSpy, { command: "overlayPageBlurred" }); - - expect(overlayBackground["checkOverlayButtonFocused"]).toHaveBeenCalled(); - }); - }); - - describe("unlockVault", () => { - it("closes the autofill overlay and opens the unlock popout", async () => { - jest.spyOn(overlayBackground as any, "closeOverlay").mockImplementation(); - jest.spyOn(overlayBackground as any, "openUnlockPopout").mockImplementation(); - jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); - - sendPortMessage(listPortSpy, { command: "unlockVault" }); - await flushPromises(); - - expect(overlayBackground["closeOverlay"]).toHaveBeenCalledWith(listPortSpy); - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - listPortSpy.sender.tab, - "addToLockedVaultPendingNotifications", - { - commandToRetry: { - message: { command: "openAutofillOverlay" }, - sender: listPortSpy.sender, - }, - target: "overlay.background", - }, - ); - expect(overlayBackground["openUnlockPopout"]).toHaveBeenCalledWith( - listPortSpy.sender.tab, - true, - ); - }); - }); - - describe("fillSelectedListItem", () => { - let getLoginCiphersSpy: jest.SpyInstance; - let isPasswordRepromptRequiredSpy: jest.SpyInstance; - let doAutoFillSpy: jest.SpyInstance; - let sender: chrome.runtime.MessageSender; - const pageDetails = createAutofillPageDetailsMock({ - login: { username: "username1", password: "password1" }, - }); - - beforeEach(() => { - getLoginCiphersSpy = jest.spyOn(overlayBackground["overlayLoginCiphers"], "get"); - isPasswordRepromptRequiredSpy = jest.spyOn( - overlayBackground["autofillService"], - "isPasswordRepromptRequired", - ); - doAutoFillSpy = jest.spyOn(overlayBackground["autofillService"], "doAutoFill"); - sender = mock({ tab: { id: 1 } }); - }); - - it("ignores the fill request if the overlay cipher id is not provided", async () => { - sendPortMessage(listPortSpy, { command: "fillSelectedListItem" }); - await flushPromises(); - - expect(getLoginCiphersSpy).not.toHaveBeenCalled(); - expect(isPasswordRepromptRequiredSpy).not.toHaveBeenCalled(); - expect(doAutoFillSpy).not.toHaveBeenCalled(); - }); - - it("ignores the fill request if the tab does not contain any identified page details", async () => { - sendPortMessage(listPortSpy, { - command: "fillSelectedListItem", - overlayCipherId: "overlay-cipher-1", - }); - await flushPromises(); - - expect(getLoginCiphersSpy).not.toHaveBeenCalled(); - expect(isPasswordRepromptRequiredSpy).not.toHaveBeenCalled(); - expect(doAutoFillSpy).not.toHaveBeenCalled(); - }); - - it("ignores the fill request if a master password reprompt is required", async () => { - const cipher = mock({ - reprompt: CipherRepromptType.Password, - type: CipherType.Login, - }); - overlayBackground["overlayLoginCiphers"] = new Map([["overlay-cipher-1", cipher]]); - overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ - [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails }], - ]); - getLoginCiphersSpy = jest.spyOn(overlayBackground["overlayLoginCiphers"], "get"); - isPasswordRepromptRequiredSpy.mockResolvedValue(true); - - sendPortMessage(listPortSpy, { - command: "fillSelectedListItem", - overlayCipherId: "overlay-cipher-1", - }); - await flushPromises(); - - expect(getLoginCiphersSpy).toHaveBeenCalled(); - expect(isPasswordRepromptRequiredSpy).toHaveBeenCalledWith( - cipher, - listPortSpy.sender.tab, - ); - expect(doAutoFillSpy).not.toHaveBeenCalled(); - }); - - it("autofills the selected cipher and move it to the top of the front of the ciphers map", async () => { - const cipher1 = mock({ id: "overlay-cipher-1" }); - const cipher2 = mock({ id: "overlay-cipher-2" }); - const cipher3 = mock({ id: "overlay-cipher-3" }); - overlayBackground["overlayLoginCiphers"] = new Map([ - ["overlay-cipher-1", cipher1], - ["overlay-cipher-2", cipher2], - ["overlay-cipher-3", cipher3], - ]); - const pageDetailsForTab = { - frameId: sender.frameId, - tab: sender.tab, - details: pageDetails, - }; - overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ - [sender.frameId, pageDetailsForTab], - ]); - isPasswordRepromptRequiredSpy.mockResolvedValue(false); - - sendPortMessage(listPortSpy, { - command: "fillSelectedListItem", - overlayCipherId: "overlay-cipher-2", - }); - await flushPromises(); - - expect(isPasswordRepromptRequiredSpy).toHaveBeenCalledWith( - cipher2, - listPortSpy.sender.tab, - ); - expect(doAutoFillSpy).toHaveBeenCalledWith({ - tab: listPortSpy.sender.tab, - cipher: cipher2, - pageDetails: [pageDetailsForTab], - fillNewPassword: true, - allowTotpAutofill: true, - }); - expect(overlayBackground["overlayLoginCiphers"].entries()).toStrictEqual( - new Map([ - ["overlay-cipher-2", cipher2], - ["overlay-cipher-1", cipher1], - ["overlay-cipher-3", cipher3], - ]).entries(), - ); - }); - - it("copies the cipher's totp code to the clipboard after filling", async () => { - const cipher1 = mock({ id: "overlay-cipher-1" }); - overlayBackground["overlayLoginCiphers"] = new Map([["overlay-cipher-1", cipher1]]); - overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ - [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails }], - ]); - isPasswordRepromptRequiredSpy.mockResolvedValue(false); - const copyToClipboardSpy = jest - .spyOn(overlayBackground["platformUtilsService"], "copyToClipboard") - .mockImplementation(); - doAutoFillSpy.mockReturnValueOnce("totp-code"); - - sendPortMessage(listPortSpy, { - command: "fillSelectedListItem", - overlayCipherId: "overlay-cipher-2", - }); - await flushPromises(); - - expect(copyToClipboardSpy).toHaveBeenCalledWith("totp-code"); - }); - }); - - describe("getNewVaultItemDetails", () => { - it("will send an addNewVaultItemFromOverlay message", async () => { - jest.spyOn(BrowserApi, "tabSendMessage"); - - sendPortMessage(listPortSpy, { command: "addNewVaultItem" }); - await flushPromises(); - - expect(BrowserApi.tabSendMessage).toHaveBeenCalledWith(listPortSpy.sender.tab, { - command: "addNewVaultItemFromOverlay", - }); - }); - }); - - describe("viewSelectedCipher", () => { - let openViewVaultItemPopoutSpy: jest.SpyInstance; - - beforeEach(() => { - openViewVaultItemPopoutSpy = jest - .spyOn(overlayBackground as any, "openViewVaultItemPopout") - .mockImplementation(); - }); - - it("returns early if the passed cipher ID does not match one of the overlay login ciphers", async () => { - overlayBackground["overlayLoginCiphers"] = new Map([ - ["overlay-cipher-0", mock({ id: "overlay-cipher-0" })], - ]); - - sendPortMessage(listPortSpy, { - command: "viewSelectedCipher", - overlayCipherId: "overlay-cipher-1", - }); - await flushPromises(); - - expect(openViewVaultItemPopoutSpy).not.toHaveBeenCalled(); - }); - - it("will open the view vault item popout with the selected cipher", async () => { - const cipher = mock({ id: "overlay-cipher-1" }); - overlayBackground["overlayLoginCiphers"] = new Map([ - ["overlay-cipher-0", mock({ id: "overlay-cipher-0" })], - ["overlay-cipher-1", cipher], - ]); - - sendPortMessage(listPortSpy, { - command: "viewSelectedCipher", - overlayCipherId: "overlay-cipher-1", - }); - await flushPromises(); - - expect(overlayBackground["openViewVaultItemPopout"]).toHaveBeenCalledWith( - listPortSpy.sender.tab, - { - cipherId: cipher.id, - action: SHOW_AUTOFILL_BUTTON, - }, - ); - }); - }); - - describe("redirectOverlayFocusOut", () => { - it("redirects focus out of the overlay list", async () => { - const message = { - command: "redirectOverlayFocusOut", - direction: RedirectFocusDirection.Next, - }; - const redirectOverlayFocusOutSpy = jest.spyOn( - overlayBackground as any, - "redirectOverlayFocusOut", - ); - - sendPortMessage(listPortSpy, message); - await flushPromises(); - - expect(redirectOverlayFocusOutSpy).toHaveBeenCalledWith(message, listPortSpy); - }); - }); - }); - }); -}); diff --git a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts deleted file mode 100644 index c9eb442d75d..00000000000 --- a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts +++ /dev/null @@ -1,811 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { firstValueFrom, map } from "rxjs"; - -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 { SHOW_AUTOFILL_BUTTON } from "@bitwarden/common/autofill/constants"; -import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; -import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; -import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { CipherType } from "@bitwarden/common/vault/enums"; -import { buildCipherIcon } from "@bitwarden/common/vault/icon/build-cipher-icon"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; -import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; - -import { openUnlockPopout } from "../../../auth/popup/utils/auth-popout-window"; -import { BrowserApi } from "../../../platform/browser/browser-api"; -import { - openViewVaultItemPopout, - openAddEditVaultItemPopout, -} from "../../../vault/popup/utils/vault-popout-window"; -import { LockedVaultPendingNotificationsData } from "../../background/abstractions/notification.background"; -import { OverlayBackground as OverlayBackgroundInterface } from "../../background/abstractions/overlay.background"; -import { AutofillOverlayElement, AutofillOverlayPort } from "../../enums/autofill-overlay.enum"; -import { AutofillService, PageDetail } from "../../services/abstractions/autofill.service"; - -import { - FocusedFieldData, - OverlayBackgroundExtensionMessageHandlers, - OverlayButtonPortMessageHandlers, - OverlayCipherData, - OverlayListPortMessageHandlers, - OverlayBackgroundExtensionMessage, - OverlayAddNewItemMessage, - OverlayPortMessage, - WebsiteIconData, -} from "./abstractions/overlay.background.deprecated"; - -class LegacyOverlayBackground implements OverlayBackgroundInterface { - private readonly openUnlockPopout = openUnlockPopout; - private readonly openViewVaultItemPopout = openViewVaultItemPopout; - private readonly openAddEditVaultItemPopout = openAddEditVaultItemPopout; - private overlayLoginCiphers: Map = new Map(); - private pageDetailsForTab: Record< - chrome.runtime.MessageSender["tab"]["id"], - Map - > = {}; - private userAuthStatus: AuthenticationStatus = AuthenticationStatus.LoggedOut; - private overlayButtonPort: chrome.runtime.Port; - private overlayListPort: chrome.runtime.Port; - private expiredPorts: chrome.runtime.Port[] = []; - private focusedFieldData: FocusedFieldData; - private overlayPageTranslations: Record; - private iconsServerUrl: string; - private readonly extensionMessageHandlers: OverlayBackgroundExtensionMessageHandlers = { - openAutofillOverlay: () => this.openOverlay(false), - autofillOverlayElementClosed: ({ message, sender }) => - this.overlayElementClosed(message, sender), - autofillOverlayAddNewVaultItem: ({ message, sender }) => this.addNewVaultItem(message, sender), - getAutofillOverlayVisibility: () => this.getOverlayVisibility(), - checkAutofillOverlayFocused: () => this.checkOverlayFocused(), - focusAutofillOverlayList: () => this.focusOverlayList(), - updateAutofillOverlayPosition: ({ message, sender }) => - this.updateOverlayPosition(message, sender), - updateAutofillOverlayHidden: ({ message }) => this.updateOverlayHidden(message), - updateFocusedFieldData: ({ message, sender }) => this.setFocusedFieldData(message, sender), - collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender), - unlockCompleted: ({ message }) => this.unlockCompleted(message), - addedCipher: () => this.updateOverlayCiphers(), - addEditCipherSubmitted: () => this.updateOverlayCiphers(), - editedCipher: () => this.updateOverlayCiphers(), - deletedCipher: () => this.updateOverlayCiphers(), - }; - private readonly overlayButtonPortMessageHandlers: OverlayButtonPortMessageHandlers = { - overlayButtonClicked: ({ port }) => this.handleOverlayButtonClicked(port), - closeAutofillOverlay: ({ port }) => this.closeOverlay(port), - forceCloseAutofillOverlay: ({ port }) => this.closeOverlay(port, true), - overlayPageBlurred: () => this.checkOverlayListFocused(), - redirectOverlayFocusOut: ({ message, port }) => this.redirectOverlayFocusOut(message, port), - }; - private readonly overlayListPortMessageHandlers: OverlayListPortMessageHandlers = { - checkAutofillOverlayButtonFocused: () => this.checkOverlayButtonFocused(), - forceCloseAutofillOverlay: ({ port }) => this.closeOverlay(port, true), - overlayPageBlurred: () => this.checkOverlayButtonFocused(), - unlockVault: ({ port }) => this.unlockVault(port), - fillSelectedListItem: ({ message, port }) => this.fillSelectedOverlayListItem(message, port), - addNewVaultItem: ({ port }) => this.getNewVaultItemDetails(port), - viewSelectedCipher: ({ message, port }) => this.viewSelectedCipher(message, port), - redirectOverlayFocusOut: ({ message, port }) => this.redirectOverlayFocusOut(message, port), - }; - - constructor( - private cipherService: CipherService, - private autofillService: AutofillService, - private authService: AuthService, - private environmentService: EnvironmentService, - private domainSettingsService: DomainSettingsService, - private autofillSettingsService: AutofillSettingsServiceAbstraction, - private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, - private themeStateService: ThemeStateService, - private accountService: AccountService, - ) {} - - /** - * Removes cached page details for a tab - * based on the passed tabId. - * - * @param tabId - Used to reference the page details of a specific tab - */ - removePageDetails(tabId: number) { - if (!this.pageDetailsForTab[tabId]) { - return; - } - - this.pageDetailsForTab[tabId].clear(); - delete this.pageDetailsForTab[tabId]; - } - - /** - * Sets up the extension message listeners and gets the settings for the - * overlay's visibility and the user's authentication status. - */ - async init() { - this.setupExtensionMessageListeners(); - const env = await firstValueFrom(this.environmentService.environment$); - this.iconsServerUrl = env.getIconsUrl(); - await this.getOverlayVisibility(); - await this.getAuthStatus(); - } - - /** - * Updates the overlay list's ciphers and sends the updated list to the overlay list iframe. - * Queries all ciphers for the given url, and sorts them by last used. Will not update the - * list of ciphers if the extension is not unlocked. - */ - async updateOverlayCiphers() { - const authStatus = await firstValueFrom(this.authService.activeAccountStatus$); - if (authStatus !== AuthenticationStatus.Unlocked) { - return; - } - - const currentTab = await BrowserApi.getTabFromCurrentWindowId(); - if (!currentTab?.url) { - return; - } - - this.overlayLoginCiphers = new Map(); - - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); - const ciphersViews = ( - await this.cipherService.getAllDecryptedForUrl(currentTab.url, activeUserId) - ).sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b)); - for (let cipherIndex = 0; cipherIndex < ciphersViews.length; cipherIndex++) { - this.overlayLoginCiphers.set(`overlay-cipher-${cipherIndex}`, ciphersViews[cipherIndex]); - } - - const ciphers = await this.getOverlayCipherData(); - this.overlayListPort?.postMessage({ command: "updateOverlayListCiphers", ciphers }); - await BrowserApi.tabSendMessageData(currentTab, "updateIsOverlayCiphersPopulated", { - isOverlayCiphersPopulated: Boolean(ciphers.length), - }); - } - - /** - * Strips out unnecessary data from the ciphers and returns an array of - * objects that contain the cipher data needed for the overlay list. - */ - private async getOverlayCipherData(): Promise { - const showFavicons = await firstValueFrom(this.domainSettingsService.showFavicons$); - const overlayCiphersArray = Array.from(this.overlayLoginCiphers); - const overlayCipherData: OverlayCipherData[] = []; - let loginCipherIcon: WebsiteIconData; - - for (let cipherIndex = 0; cipherIndex < overlayCiphersArray.length; cipherIndex++) { - const [overlayCipherId, cipher] = overlayCiphersArray[cipherIndex]; - if (!loginCipherIcon && cipher.type === CipherType.Login) { - loginCipherIcon = buildCipherIcon(this.iconsServerUrl, cipher, showFavicons); - } - - overlayCipherData.push({ - id: overlayCipherId, - name: cipher.name, - type: cipher.type, - reprompt: cipher.reprompt, - favorite: cipher.favorite, - icon: - cipher.type === CipherType.Login - ? loginCipherIcon - : buildCipherIcon(this.iconsServerUrl, cipher, showFavicons), - login: cipher.type === CipherType.Login ? { username: cipher.login.username } : null, - card: cipher.type === CipherType.Card ? cipher.card.subTitle : null, - }); - } - - return overlayCipherData; - } - - /** - * Handles aggregation of page details for a tab. Stores the page details - * in association with the tabId of the tab that sent the message. - * - * @param message - Message received from the `collectPageDetailsResponse` command - * @param sender - The sender of the message - */ - private storePageDetails( - message: OverlayBackgroundExtensionMessage, - sender: chrome.runtime.MessageSender, - ) { - const pageDetails = { - frameId: sender.frameId, - tab: sender.tab, - details: message.details, - }; - - const pageDetailsMap = this.pageDetailsForTab[sender.tab.id]; - if (!pageDetailsMap) { - this.pageDetailsForTab[sender.tab.id] = new Map([[sender.frameId, pageDetails]]); - return; - } - - pageDetailsMap.set(sender.frameId, pageDetails); - } - - /** - * Triggers autofill for the selected cipher in the overlay list. Also places - * the selected cipher at the top of the list of ciphers. - * - * @param overlayCipherId - Cipher ID corresponding to the overlayLoginCiphers map. Does not correspond to the actual cipher's ID. - * @param sender - The sender of the port message - */ - private async fillSelectedOverlayListItem( - { overlayCipherId }: OverlayPortMessage, - { sender }: chrome.runtime.Port, - ) { - const pageDetails = this.pageDetailsForTab[sender.tab.id]; - if (!overlayCipherId || !pageDetails?.size) { - return; - } - - const cipher = this.overlayLoginCiphers.get(overlayCipherId); - - if (await this.autofillService.isPasswordRepromptRequired(cipher, sender.tab)) { - return; - } - const totpCode = await this.autofillService.doAutoFill({ - tab: sender.tab, - cipher: cipher, - pageDetails: Array.from(pageDetails.values()), - fillNewPassword: true, - allowTotpAutofill: true, - }); - - if (totpCode) { - this.platformUtilsService.copyToClipboard(totpCode); - } - - this.overlayLoginCiphers = new Map([[overlayCipherId, cipher], ...this.overlayLoginCiphers]); - } - - /** - * Checks if the overlay is focused. Will check the overlay list - * if it is open, otherwise it will check the overlay button. - */ - private checkOverlayFocused() { - if (this.overlayListPort) { - this.checkOverlayListFocused(); - - return; - } - - this.checkOverlayButtonFocused(); - } - - /** - * Posts a message to the overlay button iframe to check if it is focused. - */ - private checkOverlayButtonFocused() { - this.overlayButtonPort?.postMessage({ command: "checkAutofillOverlayButtonFocused" }); - } - - /** - * Posts a message to the overlay list iframe to check if it is focused. - */ - private checkOverlayListFocused() { - this.overlayListPort?.postMessage({ command: "checkAutofillOverlayListFocused" }); - } - - /** - * Sends a message to the sender tab to close the autofill overlay. - * - * @param sender - The sender of the port message - * @param forceCloseOverlay - Identifies whether the overlay should be force closed - */ - private closeOverlay({ sender }: chrome.runtime.Port, forceCloseOverlay = false) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserApi.tabSendMessageData(sender.tab, "closeAutofillOverlay", { forceCloseOverlay }); - } - - /** - * Handles cleanup when an overlay element is closed. Disconnects - * the list and button ports and sets them to null. - * - * @param overlayElement - The overlay element that was closed, either the list or button - * @param sender - The sender of the port message - */ - private overlayElementClosed( - { overlayElement }: OverlayBackgroundExtensionMessage, - sender: chrome.runtime.MessageSender, - ) { - if (sender.tab.id !== this.focusedFieldData?.tabId) { - this.expiredPorts.forEach((port) => port.disconnect()); - this.expiredPorts = []; - return; - } - - if (overlayElement === AutofillOverlayElement.Button) { - this.overlayButtonPort?.disconnect(); - this.overlayButtonPort = null; - - return; - } - - this.overlayListPort?.disconnect(); - this.overlayListPort = null; - } - - /** - * Updates the position of either the overlay list or button. The position - * is based on the focused field's position and dimensions. - * - * @param overlayElement - The overlay element to update, either the list or button - * @param sender - The sender of the port message - */ - private updateOverlayPosition( - { overlayElement }: { overlayElement?: string }, - sender: chrome.runtime.MessageSender, - ) { - if (!overlayElement || sender.tab.id !== this.focusedFieldData?.tabId) { - return; - } - - if (overlayElement === AutofillOverlayElement.Button) { - this.overlayButtonPort?.postMessage({ - command: "updateIframePosition", - styles: this.getOverlayButtonPosition(), - }); - - return; - } - - this.overlayListPort?.postMessage({ - command: "updateIframePosition", - styles: this.getOverlayListPosition(), - }); - } - - /** - * Gets the position of the focused field and calculates the position - * of the overlay button based on the focused field's position and dimensions. - */ - private getOverlayButtonPosition() { - if (!this.focusedFieldData) { - return; - } - - const { top, left, width, height } = this.focusedFieldData.focusedFieldRects; - const { paddingRight, paddingLeft } = this.focusedFieldData.focusedFieldStyles; - let elementOffset = height * 0.37; - if (height >= 35) { - elementOffset = height >= 50 ? height * 0.47 : height * 0.42; - } - - const elementHeight = height - elementOffset; - const elementTopPosition = top + elementOffset / 2; - let elementLeftPosition = left + width - height + elementOffset / 2; - - const fieldPaddingRight = parseInt(paddingRight, 10); - const fieldPaddingLeft = parseInt(paddingLeft, 10); - if (fieldPaddingRight > fieldPaddingLeft) { - elementLeftPosition = left + width - height - (fieldPaddingRight - elementOffset + 2); - } - - return { - top: `${Math.round(elementTopPosition)}px`, - left: `${Math.round(elementLeftPosition)}px`, - height: `${Math.round(elementHeight)}px`, - width: `${Math.round(elementHeight)}px`, - }; - } - - /** - * Gets the position of the focused field and calculates the position - * of the overlay list based on the focused field's position and dimensions. - */ - private getOverlayListPosition() { - if (!this.focusedFieldData) { - return; - } - - const { top, left, width, height } = this.focusedFieldData.focusedFieldRects; - return { - width: `${Math.round(width)}px`, - top: `${Math.round(top + height)}px`, - left: `${Math.round(left)}px`, - }; - } - - /** - * Sets the focused field data to the data passed in the extension message. - * - * @param focusedFieldData - Contains the rects and styles of the focused field. - * @param sender - The sender of the extension message - */ - private setFocusedFieldData( - { focusedFieldData }: OverlayBackgroundExtensionMessage, - sender: chrome.runtime.MessageSender, - ) { - this.focusedFieldData = { ...focusedFieldData, tabId: sender.tab.id }; - } - - /** - * Updates the overlay's visibility based on the display property passed in the extension message. - * - * @param display - The display property of the overlay, either "block" or "none" - */ - private updateOverlayHidden({ display }: OverlayBackgroundExtensionMessage) { - if (!display) { - return; - } - - const portMessage = { command: "updateOverlayHidden", styles: { display } }; - - this.overlayButtonPort?.postMessage(portMessage); - this.overlayListPort?.postMessage(portMessage); - } - - /** - * Sends a message to the currently active tab to open the autofill overlay. - * - * @param isFocusingFieldElement - Identifies whether the field element should be focused when the overlay is opened - * @param isOpeningFullOverlay - Identifies whether the full overlay should be forced open regardless of other states - */ - private async openOverlay(isFocusingFieldElement = false, isOpeningFullOverlay = false) { - const currentTab = await BrowserApi.getTabFromCurrentWindowId(); - - await BrowserApi.tabSendMessageData(currentTab, "openAutofillOverlay", { - isFocusingFieldElement, - isOpeningFullOverlay, - authStatus: await this.getAuthStatus(), - }); - } - - /** - * Gets the overlay's visibility setting from the settings service. - */ - private async getOverlayVisibility(): Promise { - return await firstValueFrom(this.autofillSettingsService.inlineMenuVisibility$); - } - - /** - * Gets the user's authentication status from the auth service. If the user's - * authentication status has changed, the overlay button's authentication status - * will be updated and the overlay list's ciphers will be updated. - */ - private async getAuthStatus() { - const formerAuthStatus = this.userAuthStatus; - this.userAuthStatus = await this.authService.getAuthStatus(); - - if ( - this.userAuthStatus !== formerAuthStatus && - this.userAuthStatus === AuthenticationStatus.Unlocked - ) { - this.updateOverlayButtonAuthStatus(); - await this.updateOverlayCiphers(); - } - - return this.userAuthStatus; - } - - /** - * Sends a message to the overlay button to update its authentication status. - */ - private updateOverlayButtonAuthStatus() { - this.overlayButtonPort?.postMessage({ - command: "updateOverlayButtonAuthStatus", - authStatus: this.userAuthStatus, - }); - } - - /** - * Handles the overlay button being clicked. If the user is not authenticated, - * the vault will be unlocked. If the user is authenticated, the overlay will - * be opened. - * - * @param port - The port of the overlay button - */ - private handleOverlayButtonClicked(port: chrome.runtime.Port) { - if (this.userAuthStatus !== AuthenticationStatus.Unlocked) { - // 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.unlockVault(port); - return; - } - - // 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.openOverlay(false, true); - } - - /** - * Facilitates opening the unlock popout window. - * - * @param port - The port of the overlay list - */ - private async unlockVault(port: chrome.runtime.Port) { - const { sender } = port; - - this.closeOverlay(port); - const retryMessage: LockedVaultPendingNotificationsData = { - commandToRetry: { message: { command: "openAutofillOverlay" }, sender }, - target: "overlay.background", - }; - await BrowserApi.tabSendMessageData( - sender.tab, - "addToLockedVaultPendingNotifications", - retryMessage, - ); - await this.openUnlockPopout(sender.tab, true); - } - - /** - * Triggers the opening of a vault item popout window associated - * with the passed cipher ID. - * @param overlayCipherId - Cipher ID corresponding to the overlayLoginCiphers map. Does not correspond to the actual cipher's ID. - * @param sender - The sender of the port message - */ - private async viewSelectedCipher( - { overlayCipherId }: OverlayPortMessage, - { sender }: chrome.runtime.Port, - ) { - const cipher = this.overlayLoginCiphers.get(overlayCipherId); - if (!cipher) { - return; - } - - await this.openViewVaultItemPopout(sender.tab, { - cipherId: cipher.id, - action: SHOW_AUTOFILL_BUTTON, - }); - } - - /** - * Facilitates redirecting focus to the overlay list. - */ - private focusOverlayList() { - this.overlayListPort?.postMessage({ command: "focusOverlayList" }); - } - - /** - * Updates the authentication status for the user and opens the overlay if - * a followup command is present in the message. - * - * @param message - Extension message received from the `unlockCompleted` command - */ - private async unlockCompleted(message: OverlayBackgroundExtensionMessage) { - await this.getAuthStatus(); - - if (message.data?.commandToRetry?.message?.command === "openAutofillOverlay") { - await this.openOverlay(true); - } - } - - /** - * Gets the translations for the overlay page. - */ - private getTranslations() { - if (!this.overlayPageTranslations) { - this.overlayPageTranslations = { - locale: BrowserApi.getUILanguage(), - opensInANewWindow: this.i18nService.translate("opensInANewWindow"), - buttonPageTitle: this.i18nService.translate("bitwardenOverlayButton"), - toggleBitwardenVaultOverlay: this.i18nService.translate("toggleBitwardenVaultOverlay"), - listPageTitle: this.i18nService.translate("bitwardenVault"), - unlockYourAccount: this.i18nService.translate("unlockYourAccountToViewMatchingLogins"), - unlockAccount: this.i18nService.translate("unlockAccount"), - fillCredentialsFor: this.i18nService.translate("fillCredentialsFor"), - partialUsername: this.i18nService.translate("partialUsername"), - view: this.i18nService.translate("view"), - noItemsToShow: this.i18nService.translate("noItemsToShow"), - newItem: this.i18nService.translate("newItem"), - addNewVaultItem: this.i18nService.translate("addNewVaultItem"), - }; - } - - return this.overlayPageTranslations; - } - - /** - * Facilitates redirecting focus out of one of the - * overlay elements to elements on the page. - * - * @param direction - The direction to redirect focus to (either "next", "previous" or "current) - * @param sender - The sender of the port message - */ - private redirectOverlayFocusOut( - { direction }: OverlayPortMessage, - { sender }: chrome.runtime.Port, - ) { - if (!direction) { - return; - } - - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserApi.tabSendMessageData(sender.tab, "redirectOverlayFocusOut", { direction }); - } - - /** - * Triggers adding a new vault item from the overlay. Gathers data - * input by the user before calling to open the add/edit window. - * - * @param sender - The sender of the port message - */ - private getNewVaultItemDetails({ sender }: chrome.runtime.Port) { - void BrowserApi.tabSendMessage(sender.tab, { command: "addNewVaultItemFromOverlay" }); - } - - /** - * Handles adding a new vault item from the overlay. Gathers data login - * data captured in the extension message. - * - * @param login - The login data captured from the extension message - * @param sender - The sender of the extension message - */ - private async addNewVaultItem( - { login }: OverlayAddNewItemMessage, - sender: chrome.runtime.MessageSender, - ) { - if (!login) { - return; - } - - const uriView = new LoginUriView(); - uriView.uri = login.uri; - - const loginView = new LoginView(); - loginView.uris = [uriView]; - loginView.username = login.username || ""; - loginView.password = login.password || ""; - - const cipherView = new CipherView(); - cipherView.name = (Utils.getHostname(login.uri) || login.hostname).replace(/^www\./, ""); - cipherView.folderId = null; - cipherView.type = CipherType.Login; - cipherView.login = loginView; - - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); - await this.cipherService.setAddEditCipherInfo( - { - cipher: cipherView, - collectionIds: cipherView.collectionIds, - }, - activeUserId, - ); - - await this.openAddEditVaultItemPopout(sender.tab, { cipherId: cipherView.id }); - } - - /** - * Sets up the extension message listeners for the overlay. - */ - private setupExtensionMessageListeners() { - BrowserApi.messageListener("overlay.background", this.handleExtensionMessage); - BrowserApi.addListener(chrome.runtime.onConnect, this.handlePortOnConnect); - } - - /** - * Handles extension messages sent to the extension background. - * - * @param message - The message received from the extension - * @param sender - The sender of the message - * @param sendResponse - The response to send back to the sender - */ - private handleExtensionMessage = ( - message: OverlayBackgroundExtensionMessage, - sender: chrome.runtime.MessageSender, - sendResponse: (response?: any) => void, - ) => { - const handler: CallableFunction | undefined = this.extensionMessageHandlers[message?.command]; - if (!handler) { - return null; - } - - const messageResponse = handler({ message, sender }); - if (typeof messageResponse === "undefined") { - return 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 - Promise.resolve(messageResponse).then((response) => sendResponse(response)); - return true; - }; - - /** - * Handles the connection of a port to the extension background. - * - * @param port - The port that connected to the extension background - */ - private handlePortOnConnect = async (port: chrome.runtime.Port) => { - const isOverlayListPort = port.name === AutofillOverlayPort.List; - const isOverlayButtonPort = port.name === AutofillOverlayPort.Button; - if (!isOverlayListPort && !isOverlayButtonPort) { - return; - } - - this.storeOverlayPort(port); - port.onMessage.addListener(this.handleOverlayElementPortMessage); - port.postMessage({ - command: `initAutofillOverlay${isOverlayListPort ? "List" : "Button"}`, - authStatus: await this.getAuthStatus(), - styleSheetUrl: chrome.runtime.getURL(`overlay/${isOverlayListPort ? "list" : "button"}.css`), - theme: await firstValueFrom(this.themeStateService.selectedTheme$), - translations: this.getTranslations(), - ciphers: isOverlayListPort ? await this.getOverlayCipherData() : null, - }); - this.updateOverlayPosition( - { - overlayElement: isOverlayListPort - ? AutofillOverlayElement.List - : AutofillOverlayElement.Button, - }, - port.sender, - ); - }; - - /** - * Stores the connected overlay port and sets up any existing ports to be disconnected. - * - * @param port - The port to store -| */ - private storeOverlayPort(port: chrome.runtime.Port) { - if (port.name === AutofillOverlayPort.List) { - this.storeExpiredOverlayPort(this.overlayListPort); - this.overlayListPort = port; - return; - } - - if (port.name === AutofillOverlayPort.Button) { - this.storeExpiredOverlayPort(this.overlayButtonPort); - this.overlayButtonPort = port; - } - } - - /** - * When registering a new connection, we want to ensure that the port is disconnected. - * This method places an existing port in the expiredPorts array to be disconnected - * at a later time. - * - * @param port - The port to store in the expiredPorts array - */ - private storeExpiredOverlayPort(port: chrome.runtime.Port | null) { - if (port) { - this.expiredPorts.push(port); - } - } - - /** - * Handles messages sent to the overlay list or button ports. - * - * @param message - The message received from the port - * @param port - The port that sent the message - */ - private handleOverlayElementPortMessage = ( - message: OverlayBackgroundExtensionMessage, - port: chrome.runtime.Port, - ) => { - const command = message?.command; - let handler: CallableFunction | undefined; - - if (port.name === AutofillOverlayPort.Button) { - handler = this.overlayButtonPortMessageHandlers[command]; - } - - if (port.name === AutofillOverlayPort.List) { - handler = this.overlayListPortMessageHandlers[command]; - } - - if (!handler) { - return; - } - - handler({ message, port }); - }; -} - -export default LegacyOverlayBackground; diff --git a/apps/browser/src/autofill/deprecated/content/abstractions/autofill-init.deprecated.ts b/apps/browser/src/autofill/deprecated/content/abstractions/autofill-init.deprecated.ts deleted file mode 100644 index ed422822b36..00000000000 --- a/apps/browser/src/autofill/deprecated/content/abstractions/autofill-init.deprecated.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; - -import AutofillScript from "../../../models/autofill-script"; - -type AutofillExtensionMessage = { - command: string; - tab?: chrome.tabs.Tab; - sender?: string; - fillScript?: AutofillScript; - url?: string; - pageDetailsUrl?: string; - ciphers?: any; - data?: { - authStatus?: AuthenticationStatus; - isFocusingFieldElement?: boolean; - isOverlayCiphersPopulated?: boolean; - direction?: "previous" | "next"; - isOpeningFullOverlay?: boolean; - forceCloseOverlay?: boolean; - autofillOverlayVisibility?: number; - }; -}; - -type AutofillExtensionMessageParam = { message: AutofillExtensionMessage }; - -type AutofillExtensionMessageHandlers = { - [key: string]: CallableFunction; - collectPageDetails: ({ message }: AutofillExtensionMessageParam) => void; - collectPageDetailsImmediately: ({ message }: AutofillExtensionMessageParam) => void; - fillForm: ({ message }: AutofillExtensionMessageParam) => void; - openAutofillOverlay: ({ message }: AutofillExtensionMessageParam) => void; - closeAutofillOverlay: ({ message }: AutofillExtensionMessageParam) => void; - addNewVaultItemFromOverlay: () => void; - redirectOverlayFocusOut: ({ message }: AutofillExtensionMessageParam) => void; - updateIsOverlayCiphersPopulated: ({ message }: AutofillExtensionMessageParam) => void; - bgUnlockPopoutOpened: () => void; - bgVaultItemRepromptPopoutOpened: () => void; - updateAutofillOverlayVisibility: ({ message }: AutofillExtensionMessageParam) => void; -}; - -export { AutofillExtensionMessage, AutofillExtensionMessageHandlers }; diff --git a/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.spec.ts b/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.spec.ts deleted file mode 100644 index 96d5e85ca34..00000000000 --- a/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.spec.ts +++ /dev/null @@ -1,604 +0,0 @@ -import { mock } from "jest-mock-extended"; - -import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; - -import { RedirectFocusDirection } from "../../enums/autofill-overlay.enum"; -import AutofillPageDetails from "../../models/autofill-page-details"; -import AutofillScript from "../../models/autofill-script"; -import { - flushPromises, - mockQuerySelectorAllDefinedCall, - sendMockExtensionMessage, -} from "../../spec/testing-utils"; -import AutofillOverlayContentServiceDeprecated from "../services/autofill-overlay-content.service.deprecated"; - -import { AutofillExtensionMessage } from "./abstractions/autofill-init.deprecated"; -import AutofillInitDeprecated from "./autofill-init.deprecated"; - -describe("AutofillInit", () => { - let autofillInit: AutofillInitDeprecated; - const autofillOverlayContentService = mock(); - const originalDocumentReadyState = document.readyState; - const mockQuerySelectorAll = mockQuerySelectorAllDefinedCall(); - - beforeEach(() => { - chrome.runtime.connect = jest.fn().mockReturnValue({ - onDisconnect: { - addListener: jest.fn(), - }, - }); - autofillInit = new AutofillInitDeprecated(autofillOverlayContentService); - window.IntersectionObserver = jest.fn(() => mock()); - }); - - afterEach(() => { - jest.resetModules(); - jest.clearAllMocks(); - Object.defineProperty(document, "readyState", { - value: originalDocumentReadyState, - writable: true, - }); - }); - - afterAll(() => { - mockQuerySelectorAll.mockRestore(); - }); - - describe("init", () => { - it("sets up the extension message listeners", () => { - jest.spyOn(autofillInit as any, "setupExtensionMessageListeners"); - - autofillInit.init(); - - expect(autofillInit["setupExtensionMessageListeners"]).toHaveBeenCalled(); - }); - - it("triggers a collection of page details if the document is in a `complete` ready state", () => { - jest.useFakeTimers(); - Object.defineProperty(document, "readyState", { value: "complete", writable: true }); - - autofillInit.init(); - jest.advanceTimersByTime(250); - - expect(chrome.runtime.sendMessage).toHaveBeenCalledWith( - { - command: "bgCollectPageDetails", - sender: "autofillInit", - }, - expect.any(Function), - ); - }); - - it("registers a window load listener to collect the page details if the document is not in a `complete` ready state", () => { - jest.spyOn(window, "addEventListener"); - Object.defineProperty(document, "readyState", { value: "loading", writable: true }); - - autofillInit.init(); - - expect(window.addEventListener).toHaveBeenCalledWith("load", expect.any(Function)); - }); - }); - - describe("setupExtensionMessageListeners", () => { - it("sets up a chrome runtime on message listener", () => { - jest.spyOn(chrome.runtime.onMessage, "addListener"); - - autofillInit["setupExtensionMessageListeners"](); - - expect(chrome.runtime.onMessage.addListener).toHaveBeenCalledWith( - autofillInit["handleExtensionMessage"], - ); - }); - }); - - describe("handleExtensionMessage", () => { - let message: AutofillExtensionMessage; - let sender: chrome.runtime.MessageSender; - const sendResponse = jest.fn(); - - beforeEach(() => { - message = { - command: "collectPageDetails", - tab: mock(), - sender: "sender", - }; - sender = mock(); - }); - - it("returns a undefined value if a extension message handler is not found with the given message command", () => { - message.command = "unknownCommand"; - - const response = autofillInit["handleExtensionMessage"](message, sender, sendResponse); - - expect(response).toBe(null); - }); - - it("returns a undefined value if the message handler does not return a response", async () => { - const response1 = await autofillInit["handleExtensionMessage"](message, sender, sendResponse); - await flushPromises(); - - expect(response1).not.toBe(false); - - message.command = "removeAutofillOverlay"; - message.fillScript = mock(); - - const response2 = autofillInit["handleExtensionMessage"](message, sender, sendResponse); - await flushPromises(); - - expect(response2).toBe(null); - }); - - it("returns a true value and calls sendResponse if the message handler returns a response", async () => { - message.command = "collectPageDetailsImmediately"; - const pageDetails: AutofillPageDetails = { - title: "title", - url: "http://example.com", - documentUrl: "documentUrl", - forms: {}, - fields: [], - collectedTimestamp: 0, - }; - jest - .spyOn(autofillInit["collectAutofillContentService"], "getPageDetails") - .mockResolvedValue(pageDetails); - - const response = await autofillInit["handleExtensionMessage"](message, sender, sendResponse); - await flushPromises(); - - expect(response).toBe(true); - expect(sendResponse).toHaveBeenCalledWith(pageDetails); - }); - - describe("extension message handlers", () => { - beforeEach(() => { - autofillInit.init(); - }); - - describe("collectPageDetails", () => { - it("sends the collected page details for autofill using a background script message", async () => { - const pageDetails: AutofillPageDetails = { - title: "title", - url: "http://example.com", - documentUrl: "documentUrl", - forms: {}, - fields: [], - collectedTimestamp: 0, - }; - const message = { - command: "collectPageDetails", - sender: "sender", - tab: mock(), - }; - jest - .spyOn(autofillInit["collectAutofillContentService"], "getPageDetails") - .mockResolvedValue(pageDetails); - - sendMockExtensionMessage(message, sender, sendResponse); - await flushPromises(); - - expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({ - command: "collectPageDetailsResponse", - tab: message.tab, - details: pageDetails, - sender: message.sender, - }); - }); - }); - - describe("collectPageDetailsImmediately", () => { - it("returns collected page details for autofill if set to send the details in the response", async () => { - const pageDetails: AutofillPageDetails = { - title: "title", - url: "http://example.com", - documentUrl: "documentUrl", - forms: {}, - fields: [], - collectedTimestamp: 0, - }; - jest - .spyOn(autofillInit["collectAutofillContentService"], "getPageDetails") - .mockResolvedValue(pageDetails); - - sendMockExtensionMessage( - { command: "collectPageDetailsImmediately" }, - sender, - sendResponse, - ); - await flushPromises(); - - expect(autofillInit["collectAutofillContentService"].getPageDetails).toHaveBeenCalled(); - expect(sendResponse).toBeCalledWith(pageDetails); - expect(chrome.runtime.sendMessage).not.toHaveBeenCalledWith({ - command: "collectPageDetailsResponse", - tab: message.tab, - details: pageDetails, - sender: message.sender, - }); - }); - }); - - describe("fillForm", () => { - let fillScript: AutofillScript; - beforeEach(() => { - fillScript = mock(); - jest.spyOn(autofillInit["insertAutofillContentService"], "fillForm").mockImplementation(); - }); - - it("skips calling the InsertAutofillContentService and does not fill the form if the url to fill is not equal to the current tab url", async () => { - const fillScript = mock(); - const message = { - command: "fillForm", - fillScript, - pageDetailsUrl: "https://a-different-url.com", - }; - - sendMockExtensionMessage(message); - await flushPromises(); - - expect(autofillInit["insertAutofillContentService"].fillForm).not.toHaveBeenCalledWith( - fillScript, - ); - }); - - it("calls the InsertAutofillContentService to fill the form", async () => { - sendMockExtensionMessage({ - command: "fillForm", - fillScript, - pageDetailsUrl: window.location.href, - }); - await flushPromises(); - - expect(autofillInit["insertAutofillContentService"].fillForm).toHaveBeenCalledWith( - fillScript, - ); - }); - - it("removes the overlay when filling the form", async () => { - const blurAndRemoveOverlaySpy = jest.spyOn(autofillInit as any, "blurAndRemoveOverlay"); - sendMockExtensionMessage({ - command: "fillForm", - fillScript, - pageDetailsUrl: window.location.href, - }); - await flushPromises(); - - expect(blurAndRemoveOverlaySpy).toHaveBeenCalled(); - }); - - it("updates the isCurrentlyFilling property of the overlay to true after filling", async () => { - jest.useFakeTimers(); - jest.spyOn(autofillInit as any, "updateOverlayIsCurrentlyFilling"); - jest - .spyOn(autofillInit["autofillOverlayContentService"], "focusMostRecentOverlayField") - .mockImplementation(); - - sendMockExtensionMessage({ - command: "fillForm", - fillScript, - pageDetailsUrl: window.location.href, - }); - await flushPromises(); - jest.advanceTimersByTime(300); - - expect(autofillInit["updateOverlayIsCurrentlyFilling"]).toHaveBeenNthCalledWith(1, true); - expect(autofillInit["insertAutofillContentService"].fillForm).toHaveBeenCalledWith( - fillScript, - ); - expect(autofillInit["updateOverlayIsCurrentlyFilling"]).toHaveBeenNthCalledWith(2, false); - }); - - it("skips attempting to focus the most recent field if the autofillOverlayContentService is not present", async () => { - jest.useFakeTimers(); - const newAutofillInit = new AutofillInitDeprecated(undefined); - newAutofillInit.init(); - jest.spyOn(newAutofillInit as any, "updateOverlayIsCurrentlyFilling"); - jest - .spyOn(newAutofillInit["insertAutofillContentService"], "fillForm") - .mockImplementation(); - - sendMockExtensionMessage({ - command: "fillForm", - fillScript, - pageDetailsUrl: window.location.href, - }); - await flushPromises(); - jest.advanceTimersByTime(300); - - expect(newAutofillInit["updateOverlayIsCurrentlyFilling"]).toHaveBeenNthCalledWith( - 1, - true, - ); - expect(newAutofillInit["insertAutofillContentService"].fillForm).toHaveBeenCalledWith( - fillScript, - ); - expect(newAutofillInit["updateOverlayIsCurrentlyFilling"]).not.toHaveBeenNthCalledWith( - 2, - false, - ); - }); - }); - - describe("openAutofillOverlay", () => { - const message = { - command: "openAutofillOverlay", - data: { - isFocusingFieldElement: true, - isOpeningFullOverlay: true, - authStatus: AuthenticationStatus.Unlocked, - }, - }; - - it("skips attempting to open the autofill overlay if the autofillOverlayContentService is not present", () => { - const newAutofillInit = new AutofillInitDeprecated(undefined); - newAutofillInit.init(); - - sendMockExtensionMessage(message); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - }); - - it("opens the autofill overlay", () => { - sendMockExtensionMessage(message); - - expect( - autofillInit["autofillOverlayContentService"].openAutofillOverlay, - ).toHaveBeenCalledWith({ - isFocusingFieldElement: message.data.isFocusingFieldElement, - isOpeningFullOverlay: message.data.isOpeningFullOverlay, - authStatus: message.data.authStatus, - }); - }); - }); - - describe("closeAutofillOverlay", () => { - beforeEach(() => { - autofillInit["autofillOverlayContentService"].isFieldCurrentlyFocused = false; - autofillInit["autofillOverlayContentService"].isCurrentlyFilling = false; - }); - - it("skips attempting to remove the overlay if the autofillOverlayContentService is not present", () => { - const newAutofillInit = new AutofillInitDeprecated(undefined); - newAutofillInit.init(); - jest.spyOn(newAutofillInit as any, "removeAutofillOverlay"); - - sendMockExtensionMessage({ - command: "closeAutofillOverlay", - data: { forceCloseOverlay: false }, - }); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - }); - - it("removes the autofill overlay if the message flags a forced closure", () => { - sendMockExtensionMessage({ - command: "closeAutofillOverlay", - data: { forceCloseOverlay: true }, - }); - - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlay, - ).toHaveBeenCalled(); - }); - - it("ignores the message if a field is currently focused", () => { - autofillInit["autofillOverlayContentService"].isFieldCurrentlyFocused = true; - - sendMockExtensionMessage({ command: "closeAutofillOverlay" }); - - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlayList, - ).not.toHaveBeenCalled(); - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlay, - ).not.toHaveBeenCalled(); - }); - - it("removes the autofill overlay list if the overlay is currently filling", () => { - autofillInit["autofillOverlayContentService"].isCurrentlyFilling = true; - - sendMockExtensionMessage({ command: "closeAutofillOverlay" }); - - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlayList, - ).toHaveBeenCalled(); - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlay, - ).not.toHaveBeenCalled(); - }); - - it("removes the entire overlay if the overlay is not currently filling", () => { - sendMockExtensionMessage({ command: "closeAutofillOverlay" }); - - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlayList, - ).not.toHaveBeenCalled(); - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlay, - ).toHaveBeenCalled(); - }); - }); - - describe("addNewVaultItemFromOverlay", () => { - it("will not add a new vault item if the autofillOverlayContentService is not present", () => { - const newAutofillInit = new AutofillInitDeprecated(undefined); - newAutofillInit.init(); - - sendMockExtensionMessage({ command: "addNewVaultItemFromOverlay" }); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - }); - - it("will add a new vault item", () => { - sendMockExtensionMessage({ command: "addNewVaultItemFromOverlay" }); - - expect(autofillInit["autofillOverlayContentService"].addNewVaultItem).toHaveBeenCalled(); - }); - }); - - describe("redirectOverlayFocusOut", () => { - const message = { - command: "redirectOverlayFocusOut", - data: { - direction: RedirectFocusDirection.Next, - }, - }; - - it("ignores the message to redirect focus if the autofillOverlayContentService does not exist", () => { - const newAutofillInit = new AutofillInitDeprecated(undefined); - newAutofillInit.init(); - - sendMockExtensionMessage(message); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - }); - - it("redirects the overlay focus", () => { - sendMockExtensionMessage(message); - - expect( - autofillInit["autofillOverlayContentService"].redirectOverlayFocusOut, - ).toHaveBeenCalledWith(message.data.direction); - }); - }); - - describe("updateIsOverlayCiphersPopulated", () => { - const message = { - command: "updateIsOverlayCiphersPopulated", - data: { - isOverlayCiphersPopulated: true, - }, - }; - - it("skips updating whether the ciphers are populated if the autofillOverlayContentService does note exist", () => { - const newAutofillInit = new AutofillInitDeprecated(undefined); - newAutofillInit.init(); - - sendMockExtensionMessage(message); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - }); - - it("updates whether the overlay ciphers are populated", () => { - sendMockExtensionMessage(message); - - expect(autofillInit["autofillOverlayContentService"].isOverlayCiphersPopulated).toEqual( - message.data.isOverlayCiphersPopulated, - ); - }); - }); - - describe("bgUnlockPopoutOpened", () => { - it("skips attempting to blur and remove the overlay if the autofillOverlayContentService is not present", () => { - const newAutofillInit = new AutofillInitDeprecated(undefined); - newAutofillInit.init(); - jest.spyOn(newAutofillInit as any, "removeAutofillOverlay"); - - sendMockExtensionMessage({ command: "bgUnlockPopoutOpened" }); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - expect(newAutofillInit["removeAutofillOverlay"]).not.toHaveBeenCalled(); - }); - - it("blurs the most recently focused feel and remove the autofill overlay", () => { - jest.spyOn(autofillInit["autofillOverlayContentService"], "blurMostRecentOverlayField"); - jest.spyOn(autofillInit as any, "removeAutofillOverlay"); - - sendMockExtensionMessage({ command: "bgUnlockPopoutOpened" }); - - expect( - autofillInit["autofillOverlayContentService"].blurMostRecentOverlayField, - ).toHaveBeenCalled(); - expect(autofillInit["removeAutofillOverlay"]).toHaveBeenCalled(); - }); - }); - - describe("bgVaultItemRepromptPopoutOpened", () => { - it("skips attempting to blur and remove the overlay if the autofillOverlayContentService is not present", () => { - const newAutofillInit = new AutofillInitDeprecated(undefined); - newAutofillInit.init(); - jest.spyOn(newAutofillInit as any, "removeAutofillOverlay"); - - sendMockExtensionMessage({ command: "bgVaultItemRepromptPopoutOpened" }); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - expect(newAutofillInit["removeAutofillOverlay"]).not.toHaveBeenCalled(); - }); - - it("blurs the most recently focused feel and remove the autofill overlay", () => { - jest.spyOn(autofillInit["autofillOverlayContentService"], "blurMostRecentOverlayField"); - jest.spyOn(autofillInit as any, "removeAutofillOverlay"); - - sendMockExtensionMessage({ command: "bgVaultItemRepromptPopoutOpened" }); - - expect( - autofillInit["autofillOverlayContentService"].blurMostRecentOverlayField, - ).toHaveBeenCalled(); - expect(autofillInit["removeAutofillOverlay"]).toHaveBeenCalled(); - }); - }); - - describe("updateAutofillOverlayVisibility", () => { - beforeEach(() => { - autofillInit["autofillOverlayContentService"].autofillOverlayVisibility = - AutofillOverlayVisibility.OnButtonClick; - }); - - it("skips attempting to update the overlay visibility if the autofillOverlayVisibility data value is not present", () => { - sendMockExtensionMessage({ - command: "updateAutofillOverlayVisibility", - data: {}, - }); - - expect(autofillInit["autofillOverlayContentService"].autofillOverlayVisibility).toEqual( - AutofillOverlayVisibility.OnButtonClick, - ); - }); - - it("updates the overlay visibility value", () => { - const message = { - command: "updateAutofillOverlayVisibility", - data: { - autofillOverlayVisibility: AutofillOverlayVisibility.Off, - }, - }; - - sendMockExtensionMessage(message); - - expect(autofillInit["autofillOverlayContentService"].autofillOverlayVisibility).toEqual( - message.data.autofillOverlayVisibility, - ); - }); - }); - }); - }); - - describe("destroy", () => { - it("clears the timeout used to collect page details on load", () => { - jest.spyOn(window, "clearTimeout"); - - autofillInit.init(); - autofillInit.destroy(); - - expect(window.clearTimeout).toHaveBeenCalledWith( - autofillInit["collectPageDetailsOnLoadTimeout"], - ); - }); - - it("removes the extension message listeners", () => { - autofillInit.destroy(); - - expect(chrome.runtime.onMessage.removeListener).toHaveBeenCalledWith( - autofillInit["handleExtensionMessage"], - ); - }); - - it("destroys the collectAutofillContentService", () => { - jest.spyOn(autofillInit["collectAutofillContentService"], "destroy"); - - autofillInit.destroy(); - - expect(autofillInit["collectAutofillContentService"].destroy).toHaveBeenCalled(); - }); - }); -}); diff --git a/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts b/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts deleted file mode 100644 index fac9c0852f5..00000000000 --- a/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts +++ /dev/null @@ -1,315 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { AutofillInit } from "../../content/abstractions/autofill-init"; -import AutofillPageDetails from "../../models/autofill-page-details"; -import { CollectAutofillContentService } from "../../services/collect-autofill-content.service"; -import DomElementVisibilityService from "../../services/dom-element-visibility.service"; -import { DomQueryService } from "../../services/dom-query.service"; -import InsertAutofillContentService from "../../services/insert-autofill-content.service"; -import { sendExtensionMessage } from "../../utils"; -import { LegacyAutofillOverlayContentService } from "../services/abstractions/autofill-overlay-content.service"; - -import { - AutofillExtensionMessage, - AutofillExtensionMessageHandlers, -} from "./abstractions/autofill-init.deprecated"; - -class LegacyAutofillInit implements AutofillInit { - private readonly autofillOverlayContentService: LegacyAutofillOverlayContentService | undefined; - private readonly domElementVisibilityService: DomElementVisibilityService; - private readonly collectAutofillContentService: CollectAutofillContentService; - private readonly insertAutofillContentService: InsertAutofillContentService; - private collectPageDetailsOnLoadTimeout: number | NodeJS.Timeout | undefined; - private readonly extensionMessageHandlers: AutofillExtensionMessageHandlers = { - collectPageDetails: ({ message }) => this.collectPageDetails(message), - collectPageDetailsImmediately: ({ message }) => this.collectPageDetails(message, true), - fillForm: ({ message }) => this.fillForm(message), - openAutofillOverlay: ({ message }) => this.openAutofillOverlay(message), - closeAutofillOverlay: ({ message }) => this.removeAutofillOverlay(message), - addNewVaultItemFromOverlay: () => this.addNewVaultItemFromOverlay(), - redirectOverlayFocusOut: ({ message }) => this.redirectOverlayFocusOut(message), - updateIsOverlayCiphersPopulated: ({ message }) => this.updateIsOverlayCiphersPopulated(message), - bgUnlockPopoutOpened: () => this.blurAndRemoveOverlay(), - bgVaultItemRepromptPopoutOpened: () => this.blurAndRemoveOverlay(), - updateAutofillOverlayVisibility: ({ message }) => this.updateAutofillOverlayVisibility(message), - }; - - /** - * AutofillInit constructor. Initializes the DomElementVisibilityService, - * CollectAutofillContentService and InsertAutofillContentService classes. - * - * @param autofillOverlayContentService - The autofill overlay content service, potentially undefined. - */ - constructor(autofillOverlayContentService?: LegacyAutofillOverlayContentService) { - this.autofillOverlayContentService = autofillOverlayContentService; - this.domElementVisibilityService = new DomElementVisibilityService(); - const domQueryService = new DomQueryService(); - this.collectAutofillContentService = new CollectAutofillContentService( - this.domElementVisibilityService, - domQueryService, - this.autofillOverlayContentService, - ); - this.insertAutofillContentService = new InsertAutofillContentService( - this.domElementVisibilityService, - this.collectAutofillContentService, - ); - } - - /** - * Initializes the autofill content script, setting up - * the extension message listeners. This method should - * be called once when the content script is loaded. - */ - init() { - this.setupExtensionMessageListeners(); - this.autofillOverlayContentService?.init(); - this.collectPageDetailsOnLoad(); - } - - /** - * Triggers a collection of the page details from the - * background script, ensuring that autofill is ready - * to act on the page. - */ - private collectPageDetailsOnLoad() { - const sendCollectDetailsMessage = () => { - this.clearCollectPageDetailsOnLoadTimeout(); - this.collectPageDetailsOnLoadTimeout = setTimeout( - () => sendExtensionMessage("bgCollectPageDetails", { sender: "autofillInit" }), - 250, - ); - }; - - if (globalThis.document.readyState === "complete") { - sendCollectDetailsMessage(); - } - - globalThis.addEventListener("load", sendCollectDetailsMessage); - } - - /** - * Collects the page details and sends them to the - * extension background script. If the `sendDetailsInResponse` - * parameter is set to true, the page details will be - * returned to facilitate sending the details in the - * response to the extension message. - * - * @param message - The extension message. - * @param sendDetailsInResponse - Determines whether to send the details in the response. - */ - private async collectPageDetails( - message: AutofillExtensionMessage, - sendDetailsInResponse = false, - ): Promise { - const pageDetails: AutofillPageDetails = - await this.collectAutofillContentService.getPageDetails(); - if (sendDetailsInResponse) { - return pageDetails; - } - - void chrome.runtime.sendMessage({ - command: "collectPageDetailsResponse", - tab: message.tab, - details: pageDetails, - sender: message.sender, - }); - } - - /** - * Fills the form with the given fill script. - * - * @param {AutofillExtensionMessage} message - */ - private async fillForm({ fillScript, pageDetailsUrl }: AutofillExtensionMessage) { - if ((document.defaultView || window).location.href !== pageDetailsUrl) { - return; - } - - this.blurAndRemoveOverlay(); - this.updateOverlayIsCurrentlyFilling(true); - await this.insertAutofillContentService.fillForm(fillScript); - - if (!this.autofillOverlayContentService) { - return; - } - - setTimeout(() => this.updateOverlayIsCurrentlyFilling(false), 250); - } - - /** - * Handles updating the overlay is currently filling value. - * - * @param isCurrentlyFilling - Indicates if the overlay is currently filling - */ - private updateOverlayIsCurrentlyFilling(isCurrentlyFilling: boolean) { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.isCurrentlyFilling = isCurrentlyFilling; - } - - /** - * Opens the autofill overlay. - * - * @param data - The extension message data. - */ - private openAutofillOverlay({ data }: AutofillExtensionMessage) { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.openAutofillOverlay(data); - } - - /** - * Blurs the most recent overlay field and removes the overlay. Used - * in cases where the background unlock or vault item reprompt popout - * is opened. - */ - private blurAndRemoveOverlay() { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.blurMostRecentOverlayField(); - this.removeAutofillOverlay(); - } - - /** - * Removes the autofill overlay if the field is not currently focused. - * If the autofill is currently filling, only the overlay list will be - * removed. - */ - private removeAutofillOverlay(message?: AutofillExtensionMessage) { - if (message?.data?.forceCloseOverlay) { - this.autofillOverlayContentService?.removeAutofillOverlay(); - return; - } - - if ( - !this.autofillOverlayContentService || - this.autofillOverlayContentService.isFieldCurrentlyFocused - ) { - return; - } - - if (this.autofillOverlayContentService.isCurrentlyFilling) { - this.autofillOverlayContentService.removeAutofillOverlayList(); - return; - } - - this.autofillOverlayContentService.removeAutofillOverlay(); - } - - /** - * Adds a new vault item from the overlay. - */ - private addNewVaultItemFromOverlay() { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.addNewVaultItem(); - } - - /** - * Redirects the overlay focus out of an overlay iframe. - * - * @param data - Contains the direction to redirect the focus. - */ - private redirectOverlayFocusOut({ data }: AutofillExtensionMessage) { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.redirectOverlayFocusOut(data?.direction); - } - - /** - * Updates whether the current tab has ciphers that can populate the overlay list - * - * @param data - Contains the isOverlayCiphersPopulated value - * - */ - private updateIsOverlayCiphersPopulated({ data }: AutofillExtensionMessage) { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.isOverlayCiphersPopulated = Boolean( - data?.isOverlayCiphersPopulated, - ); - } - - /** - * Updates the autofill overlay visibility. - * - * @param data - Contains the autoFillOverlayVisibility value - */ - private updateAutofillOverlayVisibility({ data }: AutofillExtensionMessage) { - if (!this.autofillOverlayContentService || isNaN(data?.autofillOverlayVisibility)) { - return; - } - - this.autofillOverlayContentService.autofillOverlayVisibility = data?.autofillOverlayVisibility; - } - - /** - * Clears the send collect details message timeout. - */ - private clearCollectPageDetailsOnLoadTimeout() { - if (this.collectPageDetailsOnLoadTimeout) { - clearTimeout(this.collectPageDetailsOnLoadTimeout); - } - } - - /** - * Sets up the extension message listeners for the content script. - */ - private setupExtensionMessageListeners() { - chrome.runtime.onMessage.addListener(this.handleExtensionMessage); - } - - /** - * Handles the extension messages sent to the content script. - * - * @param message - The extension message. - * @param sender - The message sender. - * @param sendResponse - The send response callback. - */ - private handleExtensionMessage = ( - message: AutofillExtensionMessage, - sender: chrome.runtime.MessageSender, - sendResponse: (response?: any) => void, - ): boolean => { - const command: string = message.command; - const handler: CallableFunction | undefined = this.extensionMessageHandlers[command]; - if (!handler) { - return null; - } - - const messageResponse = handler({ message, sender }); - if (typeof messageResponse === "undefined") { - return 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 - Promise.resolve(messageResponse).then((response) => sendResponse(response)); - return true; - }; - - /** - * Handles destroying the autofill init content script. Removes all - * listeners, timeouts, and object instances to prevent memory leaks. - */ - destroy() { - this.clearCollectPageDetailsOnLoadTimeout(); - chrome.runtime.onMessage.removeListener(this.handleExtensionMessage); - this.collectAutofillContentService.destroy(); - this.autofillOverlayContentService?.destroy(); - } -} - -export default LegacyAutofillInit; diff --git a/apps/browser/src/autofill/deprecated/content/bootstrap-legacy-autofill-overlay.ts b/apps/browser/src/autofill/deprecated/content/bootstrap-legacy-autofill-overlay.ts deleted file mode 100644 index 66d672172ae..00000000000 --- a/apps/browser/src/autofill/deprecated/content/bootstrap-legacy-autofill-overlay.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { setupAutofillInitDisconnectAction } from "../../utils"; -import LegacyAutofillOverlayContentService from "../services/autofill-overlay-content.service.deprecated"; - -import LegacyAutofillInit from "./autofill-init.deprecated"; - -(function (windowContext) { - if (!windowContext.bitwardenAutofillInit) { - const autofillOverlayContentService = new LegacyAutofillOverlayContentService(); - windowContext.bitwardenAutofillInit = new LegacyAutofillInit(autofillOverlayContentService); - setupAutofillInitDisconnectAction(windowContext); - - windowContext.bitwardenAutofillInit.init(); - } -})(window); diff --git a/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-button.deprecated.ts b/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-button.deprecated.ts deleted file mode 100644 index b6b22be9439..00000000000 --- a/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-button.deprecated.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; - -type OverlayButtonMessage = { command: string; colorScheme?: string }; - -type UpdateAuthStatusMessage = OverlayButtonMessage & { authStatus: AuthenticationStatus }; - -type InitAutofillOverlayButtonMessage = UpdateAuthStatusMessage & { - styleSheetUrl: string; - translations: Record; -}; - -type OverlayButtonWindowMessageHandlers = { - [key: string]: CallableFunction; - initAutofillOverlayButton: ({ message }: { message: InitAutofillOverlayButtonMessage }) => void; - checkAutofillOverlayButtonFocused: () => void; - updateAutofillOverlayButtonAuthStatus: ({ - message, - }: { - message: UpdateAuthStatusMessage; - }) => void; - updateOverlayPageColorScheme: ({ message }: { message: OverlayButtonMessage }) => void; -}; - -export { - UpdateAuthStatusMessage, - OverlayButtonMessage, - InitAutofillOverlayButtonMessage, - OverlayButtonWindowMessageHandlers, -}; diff --git a/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-iframe.service.deprecated.ts b/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-iframe.service.deprecated.ts deleted file mode 100644 index 0c4160a0709..00000000000 --- a/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-iframe.service.deprecated.ts +++ /dev/null @@ -1,33 +0,0 @@ -type AutofillOverlayIframeExtensionMessage = { - command: string; - styles?: Partial; - theme?: string; -}; - -type AutofillOverlayIframeWindowMessageHandlers = { - [key: string]: CallableFunction; - updateAutofillOverlayListHeight: (message: AutofillOverlayIframeExtensionMessage) => void; - getPageColorScheme: () => void; -}; - -type AutofillOverlayIframeExtensionMessageParam = { - message: AutofillOverlayIframeExtensionMessage; -}; - -type BackgroundPortMessageHandlers = { - [key: string]: CallableFunction; - initAutofillOverlayList: ({ message }: AutofillOverlayIframeExtensionMessageParam) => void; - updateIframePosition: ({ message }: AutofillOverlayIframeExtensionMessageParam) => void; - updateOverlayHidden: ({ message }: AutofillOverlayIframeExtensionMessageParam) => void; -}; - -interface AutofillOverlayIframeService { - initOverlayIframe(initStyles: Partial, ariaAlert?: string): void; -} - -export { - AutofillOverlayIframeExtensionMessage, - AutofillOverlayIframeWindowMessageHandlers, - BackgroundPortMessageHandlers, - AutofillOverlayIframeService, -}; diff --git a/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-list.deprecated.ts b/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-list.deprecated.ts deleted file mode 100644 index 83578b13043..00000000000 --- a/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-list.deprecated.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; - -import { OverlayCipherData } from "../../background/abstractions/overlay.background.deprecated"; - -type OverlayListMessage = { command: string }; - -type UpdateOverlayListCiphersMessage = OverlayListMessage & { - ciphers: OverlayCipherData[]; -}; - -type InitAutofillOverlayListMessage = OverlayListMessage & { - authStatus: AuthenticationStatus; - styleSheetUrl: string; - theme: string; - translations: Record; - ciphers?: OverlayCipherData[]; -}; - -type OverlayListWindowMessageHandlers = { - [key: string]: CallableFunction; - initAutofillOverlayList: ({ message }: { message: InitAutofillOverlayListMessage }) => void; - checkAutofillOverlayListFocused: () => void; - updateOverlayListCiphers: ({ message }: { message: UpdateOverlayListCiphersMessage }) => void; - focusOverlayList: () => void; -}; - -export { - UpdateOverlayListCiphersMessage, - InitAutofillOverlayListMessage, - OverlayListWindowMessageHandlers, -}; diff --git a/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-page-element.deprecated.ts b/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-page-element.deprecated.ts deleted file mode 100644 index 368ae4e7303..00000000000 --- a/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-page-element.deprecated.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { OverlayButtonWindowMessageHandlers } from "./autofill-overlay-button.deprecated"; -import { OverlayListWindowMessageHandlers } from "./autofill-overlay-list.deprecated"; - -type WindowMessageHandlers = OverlayButtonWindowMessageHandlers | OverlayListWindowMessageHandlers; - -type AutofillOverlayPageElementWindowMessage = { - [key: string]: any; - command: string; - overlayCipherId?: string; - height?: number; -}; - -export { WindowMessageHandlers, AutofillOverlayPageElementWindowMessage }; diff --git a/apps/browser/src/autofill/deprecated/overlay/iframe-content/__snapshots__/autofill-overlay-iframe.service.deprecated.spec.ts.snap b/apps/browser/src/autofill/deprecated/overlay/iframe-content/__snapshots__/autofill-overlay-iframe.service.deprecated.spec.ts.snap deleted file mode 100644 index 132bd968899..00000000000 --- a/apps/browser/src/autofill/deprecated/overlay/iframe-content/__snapshots__/autofill-overlay-iframe.service.deprecated.spec.ts.snap +++ /dev/null @@ -1,23 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`AutofillOverlayIframeService initOverlayIframe creates an aria alert element if the ariaAlert param is passed 1`] = ` -
- aria alert -
-`; - -exports[`AutofillOverlayIframeService initOverlayIframe sets up the iframe's attributes 1`] = ` -