From 7804aa118c7c9aad521a20fbeb319ba4145ee63e Mon Sep 17 00:00:00 2001 From: Kyle Denney <4227399+kdenney@users.noreply.github.com> Date: Wed, 22 Oct 2025 17:45:51 -0500 Subject: [PATCH 01/73] [PM-27206] fix: redirect from premium page if user has premium (#16990) --- .../premium/premium-vnext.component.ts | 46 +++++++++++++++---- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/apps/web/src/app/billing/individual/premium/premium-vnext.component.ts b/apps/web/src/app/billing/individual/premium/premium-vnext.component.ts index 9de9c22d3c3..61994fdb61d 100644 --- a/apps/web/src/app/billing/individual/premium/premium-vnext.component.ts +++ b/apps/web/src/app/billing/individual/premium/premium-vnext.component.ts @@ -1,21 +1,29 @@ import { CommonModule } from "@angular/common"; import { Component, DestroyRef, inject } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { combineLatest, firstValueFrom, map, Observable, of, shareReplay, switchMap } from "rxjs"; +import { ActivatedRoute, Router } from "@angular/router"; +import { + combineLatest, + firstValueFrom, + from, + map, + Observable, + of, + shareReplay, + switchMap, +} from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/platform/sync"; import { - DialogService, - ToastService, - SectionComponent, BadgeModule, - TypographyModule, + DialogService, LinkModule, + SectionComponent, + TypographyModule, } from "@bitwarden/components"; import { PricingCardComponent } from "@bitwarden/pricing"; import { I18nPipe } from "@bitwarden/ui-common"; @@ -69,14 +77,14 @@ export class PremiumVNextComponent { constructor( private accountService: AccountService, - private i18nService: I18nService, private apiService: ApiService, private dialogService: DialogService, private platformUtilsService: PlatformUtilsService, private syncService: SyncService, - private toastService: ToastService, private billingAccountProfileStateService: BillingAccountProfileStateService, private subscriptionPricingService: SubscriptionPricingService, + private router: Router, + private activatedRoute: ActivatedRoute, ) { this.isSelfHost = this.platformUtilsService.isSelfHost(); @@ -107,6 +115,23 @@ export class PremiumVNextComponent { this.hasPremiumPersonally$, ]).pipe(map(([hasOrgPremium, hasPersonalPremium]) => !hasOrgPremium && !hasPersonalPremium)); + // redirect to user subscription page if they already have premium personally + // redirect to individual vault if they already have premium from an org + combineLatest([this.hasPremiumFromAnyOrganization$, this.hasPremiumPersonally$]) + .pipe( + takeUntilDestroyed(this.destroyRef), + switchMap(([hasPremiumFromOrg, hasPremiumPersonally]) => { + if (hasPremiumPersonally) { + return from(this.navigateToSubscriptionPage()); + } + if (hasPremiumFromOrg) { + return from(this.navigateToIndividualVault()); + } + return of(true); + }), + ) + .subscribe(); + this.personalPricingTiers$ = this.subscriptionPricingService.getPersonalSubscriptionPricingTiers$(); @@ -141,6 +166,11 @@ export class PremiumVNextComponent { ); } + private navigateToSubscriptionPage = (): Promise => + this.router.navigate(["../user-subscription"], { relativeTo: this.activatedRoute }); + + private navigateToIndividualVault = (): Promise => this.router.navigate(["/vault"]); + finalizeUpgrade = async () => { await this.apiService.refreshIdentityToken(); await this.syncService.fullSync(true); From 740fe0787f50a2b6ac55f988200ad1ffdaaea73e Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Wed, 22 Oct 2025 16:18:53 -0700 Subject: [PATCH 02/73] [PM-26974] Add reprompt check when deleting ciphers in browser extension (#16907) * Add reprompt check when deleting ciphers in browser extension * RE-use existing remprompt check function. --- .../item-more-options/item-more-options.component.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts index 83535b09e66..1b8403e6024 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts @@ -267,6 +267,11 @@ export class ItemMoreOptionsComponent { } protected async delete() { + const repromptPassed = await this.passwordRepromptService.passwordRepromptCheck(this.cipher); + if (!repromptPassed) { + return; + } + const confirmed = await this.dialogService.openSimpleDialog({ title: { key: "deleteItem" }, content: { key: "deleteItemConfirmation" }, From 0ec3f661d58e0147341010d7c63b3a362a714f13 Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Wed, 22 Oct 2025 16:19:57 -0700 Subject: [PATCH 03/73] [PM-22992] Send lastKnownRevisionDate with Attachment API calls (#16862) * Add lastKnownRevisionDate to Attachment methods. * Address issues raised by Claude PR * Fix string errors * Show error to user in event of attachment upload failure * Improve error handling for missing cipher * Add unit tests for attachment lastKnownRevisionDate * Remove generic title from toast errors * Move lastKnwonRevisionDate to function input --- .../models/request/attachment.request.ts | 1 + .../vault/models/request/cipher.request.ts | 1 + .../src/vault/services/cipher.service.spec.ts | 31 +++++++ .../src/vault/services/cipher.service.ts | 13 ++- .../cipher-file-upload.service.spec.ts | 82 +++++++++++++++++++ .../file-upload/cipher-file-upload.service.ts | 1 + .../cipher-attachments.component.spec.ts | 43 ++++++++++ .../cipher-attachments.component.ts | 13 +++ 8 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 libs/common/src/vault/services/file-upload/cipher-file-upload.service.spec.ts diff --git a/libs/common/src/vault/models/request/attachment.request.ts b/libs/common/src/vault/models/request/attachment.request.ts index d058fa69d8b..80205835ab7 100644 --- a/libs/common/src/vault/models/request/attachment.request.ts +++ b/libs/common/src/vault/models/request/attachment.request.ts @@ -5,4 +5,5 @@ export class AttachmentRequest { key: string; fileSize: number; adminRequest: boolean; + lastKnownRevisionDate: Date; } diff --git a/libs/common/src/vault/models/request/cipher.request.ts b/libs/common/src/vault/models/request/cipher.request.ts index 63776c8aea6..b29d5865d2b 100644 --- a/libs/common/src/vault/models/request/cipher.request.ts +++ b/libs/common/src/vault/models/request/cipher.request.ts @@ -201,6 +201,7 @@ export class CipherRequest { this.attachments[attachment.id] = fileName; const attachmentRequest = new AttachmentRequest(); attachmentRequest.fileName = fileName; + attachmentRequest.lastKnownRevisionDate = cipher.revisionDate; if (attachment.key != null) { attachmentRequest.key = attachment.key.encryptedString; } diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index e6c22961673..85ce8bd0423 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -174,6 +174,37 @@ describe("Cipher Service", () => { expect(spy).toHaveBeenCalled(); }); + + it("should include lastKnownRevisionDate in the upload request", async () => { + const fileName = "filename"; + const fileData = new Uint8Array(10); + const testCipher = new Cipher(cipherData); + const expectedRevisionDate = "2022-01-31T12:00:00.000Z"; + + keyService.getOrgKey.mockReturnValue( + Promise.resolve(new SymmetricCryptoKey(new Uint8Array(32)) as OrgKey), + ); + keyService.makeDataEncKey.mockReturnValue( + Promise.resolve([ + new SymmetricCryptoKey(new Uint8Array(32)), + new EncString("encrypted-key"), + ] as any), + ); + + configService.checkServerMeetsVersionRequirement$.mockReturnValue(of(false)); + configService.getFeatureFlag + .calledWith(FeatureFlag.CipherKeyEncryption) + .mockResolvedValue(false); + + const uploadSpy = jest.spyOn(cipherFileUploadService, "upload").mockResolvedValue({} as any); + + await cipherService.saveAttachmentRawWithServer(testCipher, fileName, fileData, userId); + + // Verify upload was called with cipher that has revisionDate + expect(uploadSpy).toHaveBeenCalled(); + const cipherArg = uploadSpy.mock.calls[0][0]; + expect(cipherArg.revisionDate).toEqual(new Date(expectedRevisionDate)); + }); }); describe("createWithServer()", () => { diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 41f94e02cdf..8032c69ed7c 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -937,7 +937,12 @@ export class CipherService implements CipherServiceAbstraction { cipher.attachments.forEach((attachment) => { if (attachment.key == null) { attachmentPromises.push( - this.shareAttachmentWithServer(attachment, cipher.id, organizationId), + this.shareAttachmentWithServer( + attachment, + cipher.id, + organizationId, + cipher.revisionDate, + ), ); } }); @@ -1722,7 +1727,10 @@ export class CipherService implements CipherServiceAbstraction { attachmentView: AttachmentView, cipherId: string, organizationId: string, + lastKnownRevisionDate: Date, ): Promise { + const activeUserId = await firstValueFrom(this.accountService.activeAccount$); + const attachmentResponse = await this.apiService.nativeFetch( new Request(attachmentView.url, { cache: "no-store" }), ); @@ -1731,7 +1739,6 @@ export class CipherService implements CipherServiceAbstraction { } const encBuf = await EncArrayBuffer.fromResponse(attachmentResponse); - const activeUserId = await firstValueFrom(this.accountService.activeAccount$); const userKey = await this.keyService.getUserKey(activeUserId.id); const decBuf = await this.encryptService.decryptFileData(encBuf, userKey); @@ -1752,9 +1759,11 @@ export class CipherService implements CipherServiceAbstraction { const blob = new Blob([encData.buffer], { type: "application/octet-stream" }); fd.append("key", dataEncKey[1].encryptedString); fd.append("data", blob, encFileName.encryptedString); + fd.append("lastKnownRevisionDate", lastKnownRevisionDate.toISOString()); } catch (e) { if (Utils.isNode && !Utils.isBrowser) { fd.append("key", dataEncKey[1].encryptedString); + fd.append("lastKnownRevisionDate", lastKnownRevisionDate.toISOString()); fd.append( "data", Buffer.from(encData.buffer) as any, diff --git a/libs/common/src/vault/services/file-upload/cipher-file-upload.service.spec.ts b/libs/common/src/vault/services/file-upload/cipher-file-upload.service.spec.ts new file mode 100644 index 00000000000..8837f00df6b --- /dev/null +++ b/libs/common/src/vault/services/file-upload/cipher-file-upload.service.spec.ts @@ -0,0 +1,82 @@ +import { mock } from "jest-mock-extended"; + +import { ApiService } from "../../../abstractions/api.service"; +import { EncString } from "../../../key-management/crypto/models/enc-string"; +import { FileUploadService } from "../../../platform/abstractions/file-upload/file-upload.service"; +import { Utils } from "../../../platform/misc/utils"; +import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer"; +import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; +import { CipherType } from "../../enums/cipher-type"; +import { Cipher } from "../../models/domain/cipher"; +import { AttachmentUploadDataResponse } from "../../models/response/attachment-upload-data.response"; +import { CipherResponse } from "../../models/response/cipher.response"; + +import { CipherFileUploadService } from "./cipher-file-upload.service"; + +describe("CipherFileUploadService", () => { + const apiService = mock(); + const fileUploadService = mock(); + + let service: CipherFileUploadService; + + beforeEach(() => { + jest.clearAllMocks(); + + service = new CipherFileUploadService(apiService, fileUploadService); + }); + + describe("upload", () => { + it("should include lastKnownRevisionDate in the attachment request", async () => { + const cipherId = Utils.newGuid(); + const mockCipher = new Cipher({ + id: cipherId, + type: CipherType.Login, + name: "Test Cipher", + revisionDate: "2024-01-15T10:30:00.000Z", + } as any); + + const mockEncFileName = new EncString("encrypted-filename"); + const mockEncData = { + buffer: new ArrayBuffer(100), + } as unknown as EncArrayBuffer; + + const mockDataEncKey: [SymmetricCryptoKey, EncString] = [ + new SymmetricCryptoKey(new Uint8Array(32)), + new EncString("encrypted-key"), + ]; + + const mockUploadDataResponse = { + attachmentId: "attachment-id", + url: "https://upload.example.com", + fileUploadType: 0, + cipherResponse: { + id: cipherId, + type: CipherType.Login, + revisionDate: "2024-01-15T10:30:00.000Z", + } as CipherResponse, + cipherMiniResponse: null, + } as AttachmentUploadDataResponse; + + apiService.postCipherAttachment.mockResolvedValue(mockUploadDataResponse); + fileUploadService.upload.mockResolvedValue(undefined); + + await service.upload(mockCipher, mockEncFileName, mockEncData, false, mockDataEncKey); + + const callArgs = apiService.postCipherAttachment.mock.calls[0][1]; + + expect(apiService.postCipherAttachment).toHaveBeenCalledWith( + cipherId, + expect.objectContaining({ + key: "encrypted-key", + fileName: "encrypted-filename", + fileSize: 100, + adminRequest: false, + }), + ); + + // Verify lastKnownRevisionDate is set (it's converted to a Date object) + expect(callArgs.lastKnownRevisionDate).toBeDefined(); + expect(callArgs.lastKnownRevisionDate).toEqual(new Date("2024-01-15T10:30:00.000Z")); + }); + }); +}); diff --git a/libs/common/src/vault/services/file-upload/cipher-file-upload.service.ts b/libs/common/src/vault/services/file-upload/cipher-file-upload.service.ts index 2fb2746366d..8d97a921748 100644 --- a/libs/common/src/vault/services/file-upload/cipher-file-upload.service.ts +++ b/libs/common/src/vault/services/file-upload/cipher-file-upload.service.ts @@ -33,6 +33,7 @@ export class CipherFileUploadService implements CipherFileUploadServiceAbstracti fileName: encFileName.encryptedString, fileSize: encData.buffer.byteLength, adminRequest: admin, + lastKnownRevisionDate: cipher.revisionDate, }; let response: CipherResponse; diff --git a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.spec.ts b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.spec.ts index 439c651e5ad..c88ce9f0301 100644 --- a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.spec.ts +++ b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.spec.ts @@ -240,6 +240,49 @@ describe("CipherAttachmentsComponent", () => { message: "maxFileSize", }); }); + + it("shows error toast with server message when saveAttachmentWithServer fails", async () => { + const file = { size: 100 } as File; + component.attachmentForm.controls.file.setValue(file); + + const serverError = new Error("Cipher has been modified by another client"); + saveAttachmentWithServer.mockRejectedValue(serverError); + + await component.submit(); + + expect(showToast).toHaveBeenCalledWith({ + variant: "error", + message: "Cipher has been modified by another client", + }); + }); + + it("shows error toast with fallback message when error has no message property", async () => { + const file = { size: 100 } as File; + component.attachmentForm.controls.file.setValue(file); + + saveAttachmentWithServer.mockRejectedValue({ code: "UNKNOWN_ERROR" }); + + await component.submit(); + + expect(showToast).toHaveBeenCalledWith({ + variant: "error", + message: "unexpectedError", + }); + }); + + it("shows error toast with string error message", async () => { + const file = { size: 100 } as File; + component.attachmentForm.controls.file.setValue(file); + + saveAttachmentWithServer.mockRejectedValue("Network connection failed"); + + await component.submit(); + + expect(showToast).toHaveBeenCalledWith({ + variant: "error", + message: "Network connection failed", + }); + }); }); describe("success", () => { diff --git a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts index 0bcb31c7af9..9ae1c62bd3e 100644 --- a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts +++ b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts @@ -222,6 +222,19 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit { this.onUploadSuccess.emit(); } catch (e) { this.logService.error(e); + + // Extract error message from server response, fallback to generic message + let errorMessage = this.i18nService.t("unexpectedError"); + if (typeof e === "string") { + errorMessage = e; + } else if (e?.message) { + errorMessage = e.message; + } + + this.toastService.showToast({ + variant: "error", + message: errorMessage, + }); } }; From 29dccd63522f2d65aed9395a0522c43edafa03df Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Thu, 23 Oct 2025 03:28:47 +0200 Subject: [PATCH 04/73] Auth - Prefer signal & change detection (#16950) --- .../account-switcher.component.ts | 2 ++ .../account-switching/account.component.ts | 6 +++++ .../current-account.component.ts | 2 ++ .../popup/components/set-pin.component.ts | 2 ++ .../account-security.component.spec.ts | 2 ++ .../settings/account-security.component.ts | 2 ++ .../await-desktop-dialog.component.ts | 2 ++ .../extension-device-management.component.ts | 2 ++ .../src/auth/components/set-pin.component.ts | 2 ++ .../src/auth/delete-account.component.ts | 2 ++ .../accept/accept-emergency.component.ts | 2 ++ .../guards/deep-link/deep-link.guard.spec.ts | 6 +++++ .../accept-organization.component.ts | 2 ++ .../src/app/auth/recover-delete.component.ts | 2 ++ .../app/auth/recover-two-factor.component.ts | 2 ++ .../settings/account/account.component.ts | 2 ++ .../account/change-avatar-dialog.component.ts | 4 +++ .../account/change-email.component.ts | 2 ++ .../settings/account/danger-zone.component.ts | 2 ++ .../account/deauthorize-sessions.component.ts | 2 ++ .../delete-account-dialog.component.ts | 2 ++ .../settings/account/profile.component.ts | 2 ++ .../account/selectable-avatar.component.ts | 16 ++++++++++++ ...account-verify-devices-dialog.component.ts | 2 ++ .../emergency-access-confirm.component.ts | 2 ++ .../emergency-access-add-edit.component.ts | 2 ++ .../emergency-access.component.ts | 2 ++ ...rgency-access-takeover-dialog.component.ts | 4 +++ .../view/emergency-access-view.component.ts | 2 ++ .../view/emergency-view-dialog.component.ts | 2 ++ .../settings/security/api-key.component.ts | 2 ++ .../password-settings.component.ts | 2 ++ .../security/security-keys.component.ts | 2 ++ .../settings/security/security.component.ts | 2 ++ .../two-factor-recovery.component.ts | 2 ++ ...wo-factor-setup-authenticator.component.ts | 4 +++ .../two-factor-setup-duo.component.ts | 4 +++ .../two-factor-setup-email.component.ts | 4 +++ .../two-factor-setup-method-base.component.ts | 2 ++ .../two-factor-setup-webauthn.component.ts | 2 ++ .../two-factor-setup-yubikey.component.ts | 2 ++ .../two-factor/two-factor-setup.component.ts | 2 ++ .../two-factor/two-factor-verify.component.ts | 4 +++ .../auth/settings/verify-email.component.ts | 6 +++++ .../create-credential-dialog.component.ts | 2 ++ .../delete-credential-dialog.component.ts | 2 ++ .../enable-encryption-dialog.component.ts | 2 ++ .../webauthn-login-settings.component.ts | 2 ++ .../user-verification-prompt.component.ts | 2 ++ .../user-verification.component.ts | 2 ++ .../app/auth/verify-email-token.component.ts | 2 ++ .../auth/verify-recover-delete.component.ts | 2 ++ .../bit-web/src/app/auth/sso/sso.component.ts | 2 ++ .../authentication-timeout.component.ts | 2 ++ .../components/two-factor-icon.component.ts | 6 +++++ .../components/user-verification.component.ts | 4 +++ .../device-management-item-group.component.ts | 6 +++++ .../device-management-table.component.ts | 6 +++++ .../device-management.component.ts | 2 ++ .../environment-selector.component.ts | 2 ++ .../src/auth/guards/active-auth.guard.spec.ts | 2 ++ .../login-approval-dialog.component.ts | 2 ++ .../login-via-webauthn.component.ts | 2 ++ .../change-password.component.ts | 4 +++ .../set-initial-password.component.ts | 2 ++ .../fingerprint-dialog.component.ts | 2 ++ .../input-password.component.ts | 26 +++++++++++++++++++ .../login-decryption-options.component.ts | 2 ++ .../login-via-auth-request.component.ts | 2 ++ .../login-secondary-content.component.ts | 2 ++ .../auth/src/angular/login/login.component.ts | 4 +++ .../new-device-verification.component.ts | 2 ++ .../password-callout.component.ts | 6 +++++ .../password-hint/password-hint.component.ts | 2 ++ .../registration-env-selector.component.ts | 4 +++ .../registration-finish.component.ts | 2 ++ .../registration-link-expired.component.ts | 2 ++ .../registration-start-secondary.component.ts | 2 ++ .../registration-start.component.ts | 4 +++ ...self-hosted-env-config-dialog.component.ts | 2 ++ libs/auth/src/angular/sso/sso.component.ts | 2 ++ ...two-factor-auth-authenticator.component.ts | 6 +++++ .../two-factor-auth-duo.component.ts | 6 +++++ ...ctor-auth-email-component-cache.service.ts | 2 +- .../two-factor-auth-email.component.ts | 6 +++++ .../two-factor-auth-webauthn.component.ts | 6 +++++ .../two-factor-auth-yubikey.component.ts | 4 +++ ...two-factor-auth-component-cache.service.ts | 2 +- .../two-factor-auth.component.spec.ts | 2 ++ .../two-factor-auth.component.ts | 6 +++++ .../two-factor-auth.guard.spec.ts | 2 ++ .../two-factor-options.component.ts | 2 ++ .../user-verification-dialog.component.ts | 2 ++ .../user-verification-form-input.component.ts | 12 +++++++++ .../vault-timeout-input.component.ts | 4 +++ ...lt-login-via-auth-request-cache.service.ts | 2 +- 96 files changed, 311 insertions(+), 3 deletions(-) diff --git a/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts b/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts index 48fd57431a2..9e9a1ecf570 100644 --- a/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts +++ b/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts @@ -33,6 +33,8 @@ import { AccountComponent } from "./account.component"; import { CurrentAccountComponent } from "./current-account.component"; import { AccountSwitcherService } from "./services/account-switcher.service"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "account-switcher.component.html", imports: [ diff --git a/apps/browser/src/auth/popup/account-switching/account.component.ts b/apps/browser/src/auth/popup/account-switching/account.component.ts index c060d9161ef..edfad2a54b3 100644 --- a/apps/browser/src/auth/popup/account-switching/account.component.ts +++ b/apps/browser/src/auth/popup/account-switching/account.component.ts @@ -13,13 +13,19 @@ import { BiometricsService } from "@bitwarden/key-management"; import { AccountSwitcherService, AvailableAccount } from "./services/account-switcher.service"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "auth-account", templateUrl: "account.component.html", imports: [CommonModule, JslibModule, AvatarModule, ItemModule], }) export class AccountComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() account: AvailableAccount; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() loading = new EventEmitter(); constructor( diff --git a/apps/browser/src/auth/popup/account-switching/current-account.component.ts b/apps/browser/src/auth/popup/account-switching/current-account.component.ts index 63e8481621a..2dde3b5a266 100644 --- a/apps/browser/src/auth/popup/account-switching/current-account.component.ts +++ b/apps/browser/src/auth/popup/account-switching/current-account.component.ts @@ -21,6 +21,8 @@ export type CurrentAccount = { avatarColor: string; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-current-account", templateUrl: "current-account.component.html", diff --git a/apps/browser/src/auth/popup/components/set-pin.component.ts b/apps/browser/src/auth/popup/components/set-pin.component.ts index a9e8e1b122f..dbb71ae3b07 100644 --- a/apps/browser/src/auth/popup/components/set-pin.component.ts +++ b/apps/browser/src/auth/popup/components/set-pin.component.ts @@ -13,6 +13,8 @@ import { IconButtonModule, } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "set-pin.component.html", imports: [ diff --git a/apps/browser/src/auth/popup/settings/account-security.component.spec.ts b/apps/browser/src/auth/popup/settings/account-security.component.spec.ts index 2335c5c2e69..aa3639e9e93 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.spec.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.spec.ts @@ -41,6 +41,8 @@ import { PopupRouterCacheService } from "../../../platform/popup/view-cache/popu import { AccountSecurityComponent } from "./account-security.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-pop-out", template: ` `, diff --git a/apps/browser/src/auth/popup/settings/account-security.component.ts b/apps/browser/src/auth/popup/settings/account-security.component.ts index 4eb24d19605..65a0d33f93e 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.ts @@ -78,6 +78,8 @@ import { SetPinComponent } from "../components/set-pin.component"; import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "account-security.component.html", imports: [ diff --git a/apps/browser/src/auth/popup/settings/await-desktop-dialog.component.ts b/apps/browser/src/auth/popup/settings/await-desktop-dialog.component.ts index 11bb9683bb9..a64cea1ef3e 100644 --- a/apps/browser/src/auth/popup/settings/await-desktop-dialog.component.ts +++ b/apps/browser/src/auth/popup/settings/await-desktop-dialog.component.ts @@ -3,6 +3,8 @@ import { Component } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "await-desktop-dialog.component.html", imports: [JslibModule, ButtonModule, DialogModule], diff --git a/apps/browser/src/auth/popup/settings/extension-device-management.component.ts b/apps/browser/src/auth/popup/settings/extension-device-management.component.ts index 793965db141..b431fc874dd 100644 --- a/apps/browser/src/auth/popup/settings/extension-device-management.component.ts +++ b/apps/browser/src/auth/popup/settings/extension-device-management.component.ts @@ -7,6 +7,8 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ standalone: true, selector: "extension-device-management", diff --git a/apps/desktop/src/auth/components/set-pin.component.ts b/apps/desktop/src/auth/components/set-pin.component.ts index 93e1ea0d25c..5bb8e761b32 100644 --- a/apps/desktop/src/auth/components/set-pin.component.ts +++ b/apps/desktop/src/auth/components/set-pin.component.ts @@ -12,6 +12,8 @@ import { FormFieldModule, IconButtonModule, } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "set-pin.component.html", imports: [ diff --git a/apps/desktop/src/auth/delete-account.component.ts b/apps/desktop/src/auth/delete-account.component.ts index b6c6650375d..5cd73896e07 100644 --- a/apps/desktop/src/auth/delete-account.component.ts +++ b/apps/desktop/src/auth/delete-account.component.ts @@ -20,6 +20,8 @@ import { import { UserVerificationComponent } from "../app/components/user-verification.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-delete-account", templateUrl: "delete-account.component.html", diff --git a/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.ts b/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.ts index e1b7329504c..d1491e6d782 100644 --- a/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.ts +++ b/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.ts @@ -12,6 +12,8 @@ import { SharedModule } from "../../../shared"; import { EmergencyAccessModule } from "../emergency-access.module"; import { EmergencyAccessService } from "../services/emergency-access.service"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ imports: [SharedModule, EmergencyAccessModule], templateUrl: "accept-emergency.component.html", diff --git a/apps/web/src/app/auth/guards/deep-link/deep-link.guard.spec.ts b/apps/web/src/app/auth/guards/deep-link/deep-link.guard.spec.ts index dba4dbd8357..31033b29154 100644 --- a/apps/web/src/app/auth/guards/deep-link/deep-link.guard.spec.ts +++ b/apps/web/src/app/auth/guards/deep-link/deep-link.guard.spec.ts @@ -11,18 +11,24 @@ import { RouterService } from "../../../core/router.service"; import { deepLinkGuard } from "./deep-link.guard"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: "", standalone: false, }) export class GuardedRouteTestComponent {} +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: "", standalone: false, }) export class LockTestComponent {} +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: "", standalone: false, diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.component.ts b/apps/web/src/app/auth/organization-invite/accept-organization.component.ts index 09d4fc3e9ef..f98a62f91ea 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.component.ts +++ b/apps/web/src/app/auth/organization-invite/accept-organization.component.ts @@ -16,6 +16,8 @@ import { BaseAcceptComponent } from "../../common/base.accept.component"; import { AcceptOrganizationInviteService } from "./accept-organization.service"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "accept-organization.component.html", standalone: false, diff --git a/apps/web/src/app/auth/recover-delete.component.ts b/apps/web/src/app/auth/recover-delete.component.ts index 7381d526879..00b14f9a402 100644 --- a/apps/web/src/app/auth/recover-delete.component.ts +++ b/apps/web/src/app/auth/recover-delete.component.ts @@ -10,6 +10,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ToastService } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-recover-delete", templateUrl: "recover-delete.component.html", diff --git a/apps/web/src/app/auth/recover-two-factor.component.ts b/apps/web/src/app/auth/recover-two-factor.component.ts index f606e803df3..dc85668c8ec 100644 --- a/apps/web/src/app/auth/recover-two-factor.component.ts +++ b/apps/web/src/app/auth/recover-two-factor.component.ts @@ -16,6 +16,8 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { ToastService } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-recover-two-factor", templateUrl: "recover-two-factor.component.html", diff --git a/apps/web/src/app/auth/settings/account/account.component.ts b/apps/web/src/app/auth/settings/account/account.component.ts index 921db19bc49..8bae8cd2c1f 100644 --- a/apps/web/src/app/auth/settings/account/account.component.ts +++ b/apps/web/src/app/auth/settings/account/account.component.ts @@ -19,6 +19,8 @@ import { DeleteAccountDialogComponent } from "./delete-account-dialog.component" import { ProfileComponent } from "./profile.component"; import { SetAccountVerifyDevicesDialogComponent } from "./set-account-verify-devices-dialog.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "account.component.html", imports: [ diff --git a/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.ts b/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.ts index 6bb785fb8f5..6e6fac1404e 100644 --- a/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.ts +++ b/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.ts @@ -32,6 +32,8 @@ type ChangeAvatarDialogData = { profile: ProfileResponse; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "change-avatar-dialog.component.html", encapsulation: ViewEncapsulation.None, @@ -40,6 +42,8 @@ type ChangeAvatarDialogData = { export class ChangeAvatarDialogComponent implements OnInit, OnDestroy { profile: ProfileResponse; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("colorPicker") colorPickerElement: ElementRef; loading = false; diff --git a/apps/web/src/app/auth/settings/account/change-email.component.ts b/apps/web/src/app/auth/settings/account/change-email.component.ts index b6ca39c6413..ee29e0c8a9c 100644 --- a/apps/web/src/app/auth/settings/account/change-email.component.ts +++ b/apps/web/src/app/auth/settings/account/change-email.component.ts @@ -17,6 +17,8 @@ import { KdfConfigService, KeyService } from "@bitwarden/key-management"; import { SharedModule } from "../../../shared"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-change-email", templateUrl: "change-email.component.html", diff --git a/apps/web/src/app/auth/settings/account/danger-zone.component.ts b/apps/web/src/app/auth/settings/account/danger-zone.component.ts index 05fd22d087d..e60ff6ec03d 100644 --- a/apps/web/src/app/auth/settings/account/danger-zone.component.ts +++ b/apps/web/src/app/auth/settings/account/danger-zone.component.ts @@ -9,6 +9,8 @@ import { I18nPipe } from "@bitwarden/ui-common"; /** * Component for the Danger Zone section of the Account/Organization Settings page. */ +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-danger-zone", templateUrl: "danger-zone.component.html", diff --git a/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.ts b/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.ts index f75320e8335..b792963ae9b 100644 --- a/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.ts +++ b/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.ts @@ -12,6 +12,8 @@ import { DialogService, ToastService } from "@bitwarden/components"; import { SharedModule } from "../../../shared"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "deauthorize-sessions.component.html", imports: [SharedModule, UserVerificationFormInputComponent], diff --git a/apps/web/src/app/auth/settings/account/delete-account-dialog.component.ts b/apps/web/src/app/auth/settings/account/delete-account-dialog.component.ts index 7e8f169994f..76eb067fdd2 100644 --- a/apps/web/src/app/auth/settings/account/delete-account-dialog.component.ts +++ b/apps/web/src/app/auth/settings/account/delete-account-dialog.component.ts @@ -12,6 +12,8 @@ import { DialogRef, DialogService, ToastService } from "@bitwarden/components"; import { SharedModule } from "../../../shared"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "delete-account-dialog.component.html", imports: [SharedModule, UserVerificationFormInputComponent], diff --git a/apps/web/src/app/auth/settings/account/profile.component.ts b/apps/web/src/app/auth/settings/account/profile.component.ts index 1f4fa578491..fd96f343b3a 100644 --- a/apps/web/src/app/auth/settings/account/profile.component.ts +++ b/apps/web/src/app/auth/settings/account/profile.component.ts @@ -23,6 +23,8 @@ import { AccountFingerprintComponent } from "../../../shared/components/account- import { ChangeAvatarDialogComponent } from "./change-avatar-dialog.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-profile", templateUrl: "profile.component.html", diff --git a/apps/web/src/app/auth/settings/account/selectable-avatar.component.ts b/apps/web/src/app/auth/settings/account/selectable-avatar.component.ts index 630c0e949ad..b74cf8beb59 100644 --- a/apps/web/src/app/auth/settings/account/selectable-avatar.component.ts +++ b/apps/web/src/app/auth/settings/account/selectable-avatar.component.ts @@ -5,6 +5,8 @@ import { Component, EventEmitter, Input, Output } from "@angular/core"; import { AvatarModule } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "selectable-avatar", template: `(); onFire() { diff --git a/apps/web/src/app/auth/settings/account/set-account-verify-devices-dialog.component.ts b/apps/web/src/app/auth/settings/account/set-account-verify-devices-dialog.component.ts index c66f31f6c3b..01be46c45b3 100644 --- a/apps/web/src/app/auth/settings/account/set-account-verify-devices-dialog.component.ts +++ b/apps/web/src/app/auth/settings/account/set-account-verify-devices-dialog.component.ts @@ -27,6 +27,8 @@ import { ToastService, } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "./set-account-verify-devices-dialog.component.html", imports: [ diff --git a/apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.ts b/apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.ts index 641dde66cc4..a5fdd5212fa 100644 --- a/apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.ts @@ -25,6 +25,8 @@ type EmergencyAccessConfirmDialogData = { /** user public key */ publicKey: Uint8Array; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "emergency-access-confirm.component.html", imports: [SharedModule], diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts b/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts index 04b549e7f05..2e8d02a0c4f 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts @@ -35,6 +35,8 @@ export enum EmergencyAccessAddEditDialogResult { Canceled = "canceled", Deleted = "deleted", } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "emergency-access-add-edit.component.html", imports: [SharedModule, PremiumBadgeComponent], diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts index f6594f4b11a..c2b8127ec34 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts @@ -42,6 +42,8 @@ import { EmergencyAccessTakeoverDialogResultType, } from "./takeover/emergency-access-takeover-dialog.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "emergency-access.component.html", imports: [SharedModule, HeaderModule, PremiumBadgeComponent], diff --git a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover-dialog.component.ts b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover-dialog.component.ts index e5c21fb82b9..743f41537e9 100644 --- a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover-dialog.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover-dialog.component.ts @@ -48,6 +48,8 @@ export type EmergencyAccessTakeoverDialogResultType = * * @link https://bitwarden.com/help/emergency-access/ */ +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "auth-emergency-access-takeover-dialog", templateUrl: "./emergency-access-takeover-dialog.component.html", @@ -61,6 +63,8 @@ export type EmergencyAccessTakeoverDialogResultType = ], }) export class EmergencyAccessTakeoverDialogComponent implements OnInit { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild(InputPasswordComponent) inputPasswordComponent: InputPasswordComponent | undefined = undefined; diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.ts b/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.ts index 250261fb0e7..1d96a19ca74 100644 --- a/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.ts @@ -14,6 +14,8 @@ import { EmergencyAccessService } from "../../../emergency-access"; import { EmergencyViewDialogComponent } from "./emergency-view-dialog.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "emergency-access-view.component.html", providers: [{ provide: CipherFormConfigService, useClass: DefaultCipherFormConfigService }], diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts index 656ec894f27..62cfd95ecfa 100644 --- a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts @@ -35,6 +35,8 @@ class PremiumUpgradePromptNoop implements PremiumUpgradePromptService { } } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-emergency-view-dialog", templateUrl: "emergency-view-dialog.component.html", diff --git a/apps/web/src/app/auth/settings/security/api-key.component.ts b/apps/web/src/app/auth/settings/security/api-key.component.ts index 82d1010f020..af49ca556ab 100644 --- a/apps/web/src/app/auth/settings/security/api-key.component.ts +++ b/apps/web/src/app/auth/settings/security/api-key.component.ts @@ -23,6 +23,8 @@ export type ApiKeyDialogData = { apiKeyWarning: string; apiKeyDescription: string; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "api-key.component.html", imports: [SharedModule, UserVerificationFormInputComponent], diff --git a/apps/web/src/app/auth/settings/security/password-settings/password-settings.component.ts b/apps/web/src/app/auth/settings/security/password-settings/password-settings.component.ts index 0698ffe1f8d..0e37c856935 100644 --- a/apps/web/src/app/auth/settings/security/password-settings/password-settings.component.ts +++ b/apps/web/src/app/auth/settings/security/password-settings/password-settings.component.ts @@ -10,6 +10,8 @@ import { I18nPipe } from "@bitwarden/ui-common"; import { WebauthnLoginSettingsModule } from "../../webauthn-login-settings"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-password-settings", templateUrl: "password-settings.component.html", diff --git a/apps/web/src/app/auth/settings/security/security-keys.component.ts b/apps/web/src/app/auth/settings/security/security-keys.component.ts index 9d16d4380eb..27a555ff343 100644 --- a/apps/web/src/app/auth/settings/security/security-keys.component.ts +++ b/apps/web/src/app/auth/settings/security/security-keys.component.ts @@ -13,6 +13,8 @@ import { SharedModule } from "../../../shared"; import { ApiKeyComponent } from "./api-key.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "security-keys.component.html", imports: [SharedModule, ChangeKdfModule], diff --git a/apps/web/src/app/auth/settings/security/security.component.ts b/apps/web/src/app/auth/settings/security/security.component.ts index 2a237bf6d01..ff13515eec0 100644 --- a/apps/web/src/app/auth/settings/security/security.component.ts +++ b/apps/web/src/app/auth/settings/security/security.component.ts @@ -5,6 +5,8 @@ import { UserVerificationService } from "@bitwarden/common/auth/abstractions/use import { HeaderModule } from "../../../layouts/header/header.module"; import { SharedModule } from "../../../shared"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "security.component.html", imports: [SharedModule, HeaderModule], diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-recovery.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-recovery.component.ts index 37d94bfae0e..543c4236d89 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-recovery.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-recovery.component.ts @@ -15,6 +15,8 @@ import { } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-two-factor-recovery", templateUrl: "two-factor-recovery.component.html", diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-authenticator.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-authenticator.component.ts index d57d6eca894..20c3c742db6 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-authenticator.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-authenticator.component.ts @@ -53,6 +53,8 @@ declare global { } } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-two-factor-setup-authenticator", templateUrl: "two-factor-setup-authenticator.component.html", @@ -76,6 +78,8 @@ export class TwoFactorSetupAuthenticatorComponent extends TwoFactorSetupMethodBaseComponent implements OnInit, OnDestroy { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onChangeStatus = new EventEmitter(); type = TwoFactorProviderType.Authenticator; key: string; diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-duo.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-duo.component.ts index bf820e32917..1a476f2206d 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-duo.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-duo.component.ts @@ -30,6 +30,8 @@ import { I18nPipe } from "@bitwarden/ui-common"; import { TwoFactorSetupMethodBaseComponent } from "./two-factor-setup-method-base.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-two-factor-setup-duo", templateUrl: "two-factor-setup-duo.component.html", @@ -51,6 +53,8 @@ export class TwoFactorSetupDuoComponent extends TwoFactorSetupMethodBaseComponent implements OnInit { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onChangeStatus: EventEmitter = new EventEmitter(); type = TwoFactorProviderType.Duo; diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-email.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-email.component.ts index 138d541d551..4219fb0b687 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-email.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-email.component.ts @@ -33,6 +33,8 @@ import { I18nPipe } from "@bitwarden/ui-common"; import { TwoFactorSetupMethodBaseComponent } from "./two-factor-setup-method-base.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-two-factor-setup-email", templateUrl: "two-factor-setup-email.component.html", @@ -54,6 +56,8 @@ export class TwoFactorSetupEmailComponent extends TwoFactorSetupMethodBaseComponent implements OnInit { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onChangeStatus: EventEmitter = new EventEmitter(); type = TwoFactorProviderType.Email; sentEmail: string = ""; diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-method-base.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-method-base.component.ts index aa3b9e1def3..c614e45e577 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-method-base.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-method-base.component.ts @@ -17,6 +17,8 @@ import { DialogService, ToastService } from "@bitwarden/components"; */ @Directive({}) export abstract class TwoFactorSetupMethodBaseComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onUpdated = new EventEmitter(); type: TwoFactorProviderType | undefined; diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.ts index ff0e971461e..acf83ab278e 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-webauthn.component.ts @@ -43,6 +43,8 @@ interface Key { removePromise: Promise | null; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-two-factor-setup-webauthn", templateUrl: "two-factor-setup-webauthn.component.html", diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-yubikey.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-yubikey.component.ts index 4e4691a5f60..09fb1ad308f 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-yubikey.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-yubikey.component.ts @@ -44,6 +44,8 @@ interface Key { existingKey: string; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-two-factor-setup-yubikey", templateUrl: "two-factor-setup-yubikey.component.html", diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts index c3a55ad661e..ef4d647a7d0 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts @@ -45,6 +45,8 @@ import { TwoFactorSetupWebAuthnComponent } from "./two-factor-setup-webauthn.com import { TwoFactorSetupYubiKeyComponent } from "./two-factor-setup-yubikey.component"; import { TwoFactorVerifyComponent } from "./two-factor-verify.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-two-factor-setup", templateUrl: "two-factor-setup.component.html", 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 a2c734ed2d5..9baa93d38c0 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 @@ -28,6 +28,8 @@ type TwoFactorVerifyDialogData = { organizationId: string; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-two-factor-verify", templateUrl: "two-factor-verify.component.html", @@ -43,6 +45,8 @@ type TwoFactorVerifyDialogData = { export class TwoFactorVerifyComponent { type: TwoFactorProviderType; organizationId: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onAuthed = new EventEmitter>(); formPromise: Promise | undefined; diff --git a/apps/web/src/app/auth/settings/verify-email.component.ts b/apps/web/src/app/auth/settings/verify-email.component.ts index 7088dae8d0f..a63d0b18b36 100644 --- a/apps/web/src/app/auth/settings/verify-email.component.ts +++ b/apps/web/src/app/auth/settings/verify-email.component.ts @@ -16,6 +16,8 @@ import { ToastService, } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-verify-email", templateUrl: "verify-email.component.html", @@ -24,7 +26,11 @@ import { export class VerifyEmailComponent { actionPromise: Promise; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onVerified = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onDismiss = new EventEmitter(); constructor( diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts b/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts index 04b148e8a0a..89b7410baba 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts @@ -32,6 +32,8 @@ type Step = | "credentialCreationFailed" | "credentialNaming"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "create-credential-dialog.component.html", standalone: false, diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/delete-credential-dialog/delete-credential-dialog.component.ts b/apps/web/src/app/auth/settings/webauthn-login-settings/delete-credential-dialog/delete-credential-dialog.component.ts index ea766a302ca..af4b7c497fb 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/delete-credential-dialog/delete-credential-dialog.component.ts +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/delete-credential-dialog/delete-credential-dialog.component.ts @@ -24,6 +24,8 @@ export interface DeleteCredentialDialogParams { credentialId: string; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "delete-credential-dialog.component.html", standalone: false, diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.ts b/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.ts index dd1ac45a9b6..24a711cb5b4 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.ts +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/enable-encryption-dialog/enable-encryption-dialog.component.ts @@ -21,6 +21,8 @@ export interface EnableEncryptionDialogParams { credentialId: string; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "enable-encryption-dialog.component.html", standalone: false, diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.ts b/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.ts index 94e926ac138..e8a278d8dd7 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.ts +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.ts @@ -17,6 +17,8 @@ import { openCreateCredentialDialog } from "./create-credential-dialog/create-cr import { openDeleteCredentialDialogComponent } from "./delete-credential-dialog/delete-credential-dialog.component"; import { openEnableCredentialDialogComponent } from "./enable-encryption-dialog/enable-encryption-dialog.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-webauthn-login-settings", templateUrl: "webauthn-login-settings.component.html", diff --git a/apps/web/src/app/auth/shared/components/user-verification/user-verification-prompt.component.ts b/apps/web/src/app/auth/shared/components/user-verification/user-verification-prompt.component.ts index 77df374f3ed..beafa48bb8e 100644 --- a/apps/web/src/app/auth/shared/components/user-verification/user-verification-prompt.component.ts +++ b/apps/web/src/app/auth/shared/components/user-verification/user-verification-prompt.component.ts @@ -21,6 +21,8 @@ import { /** * @deprecated Jan 24, 2024: Use new libs/auth UserVerificationDialogComponent instead. */ +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "user-verification-prompt.component.html", standalone: false, diff --git a/apps/web/src/app/auth/shared/components/user-verification/user-verification.component.ts b/apps/web/src/app/auth/shared/components/user-verification/user-verification.component.ts index 42f4b26fb36..7ea5014254b 100644 --- a/apps/web/src/app/auth/shared/components/user-verification/user-verification.component.ts +++ b/apps/web/src/app/auth/shared/components/user-verification/user-verification.component.ts @@ -8,6 +8,8 @@ import { UserVerificationComponent as BaseComponent } from "@bitwarden/angular/a * @deprecated Jan 24, 2024: Use new libs/auth UserVerificationDialogComponent or UserVerificationFormInputComponent instead. * Each client specific component should eventually be converted over to use one of these new components. */ +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-user-verification", templateUrl: "user-verification.component.html", diff --git a/apps/web/src/app/auth/verify-email-token.component.ts b/apps/web/src/app/auth/verify-email-token.component.ts index 2c4fa7f447c..30bfcf95bbf 100644 --- a/apps/web/src/app/auth/verify-email-token.component.ts +++ b/apps/web/src/app/auth/verify-email-token.component.ts @@ -13,6 +13,8 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ToastService } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-verify-email-token", templateUrl: "verify-email-token.component.html", diff --git a/apps/web/src/app/auth/verify-recover-delete.component.ts b/apps/web/src/app/auth/verify-recover-delete.component.ts index a475fdfd3e5..06d6096c3de 100644 --- a/apps/web/src/app/auth/verify-recover-delete.component.ts +++ b/apps/web/src/app/auth/verify-recover-delete.component.ts @@ -11,6 +11,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ToastService } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-verify-recover-delete", templateUrl: "verify-recover-delete.component.html", diff --git a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts index 64fa36fc4ac..4928d7a6abc 100644 --- a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts +++ b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts @@ -58,6 +58,8 @@ interface SelectOptions { const defaultSigningAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-org-manage-sso", templateUrl: "sso.component.html", diff --git a/libs/angular/src/auth/components/authentication-timeout.component.ts b/libs/angular/src/auth/components/authentication-timeout.component.ts index 940798de9e7..ed1ff9d29fa 100644 --- a/libs/angular/src/auth/components/authentication-timeout.component.ts +++ b/libs/angular/src/auth/components/authentication-timeout.component.ts @@ -9,6 +9,8 @@ import { ButtonModule } from "@bitwarden/components"; * This component is used to display a message to the user that their authentication session has expired. * It provides a button to navigate to the login page. */ +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-authentication-timeout", imports: [CommonModule, JslibModule, ButtonModule, RouterModule], diff --git a/libs/angular/src/auth/components/two-factor-icon.component.ts b/libs/angular/src/auth/components/two-factor-icon.component.ts index c9c7e43b61f..85db7975f87 100644 --- a/libs/angular/src/auth/components/two-factor-icon.component.ts +++ b/libs/angular/src/auth/components/two-factor-icon.component.ts @@ -9,13 +9,19 @@ import { TwoFactorAuthWebAuthnIcon, } from "@bitwarden/assets/svg"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "auth-two-factor-icon", templateUrl: "./two-factor-icon.component.html", standalone: false, }) export class TwoFactorIconComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() provider: any; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() name: string; protected readonly IconProviderMap: { [key: number | string]: Icon } = { diff --git a/libs/angular/src/auth/components/user-verification.component.ts b/libs/angular/src/auth/components/user-verification.component.ts index 6f5021340c7..1f0659a92ff 100644 --- a/libs/angular/src/auth/components/user-verification.component.ts +++ b/libs/angular/src/auth/components/user-verification.component.ts @@ -26,6 +26,8 @@ import { KeyService } from "@bitwarden/key-management"; }) export class UserVerificationComponent implements ControlValueAccessor, OnInit, OnDestroy { private _invalidSecret = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() get invalidSecret() { return this._invalidSecret; @@ -43,6 +45,8 @@ export class UserVerificationComponent implements ControlValueAccessor, OnInit, } this.secret.updateValueAndValidity({ emitEvent: false }); } + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() invalidSecretChange = new EventEmitter(); hasMasterPassword = true; diff --git a/libs/angular/src/auth/device-management/device-management-item-group.component.ts b/libs/angular/src/auth/device-management/device-management-item-group.component.ts index 71e343e734f..71fe80928f9 100644 --- a/libs/angular/src/auth/device-management/device-management-item-group.component.ts +++ b/libs/angular/src/auth/device-management/device-management-item-group.component.ts @@ -8,6 +8,8 @@ import { I18nPipe } from "@bitwarden/ui-common"; import { DeviceDisplayData } from "./device-management.component"; /** Displays user devices in an item list view */ +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ standalone: true, selector: "auth-device-management-item-group", @@ -15,7 +17,11 @@ import { DeviceDisplayData } from "./device-management.component"; imports: [BadgeModule, CommonModule, ItemModule, I18nPipe], }) export class DeviceManagementItemGroupComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() devices: DeviceDisplayData[] = []; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onAuthRequestAnswered = new EventEmitter(); protected answerAuthRequest(pendingAuthRequest: DevicePendingAuthRequest | null) { diff --git a/libs/angular/src/auth/device-management/device-management-table.component.ts b/libs/angular/src/auth/device-management/device-management-table.component.ts index d663e28b9e4..36edf6dd336 100644 --- a/libs/angular/src/auth/device-management/device-management-table.component.ts +++ b/libs/angular/src/auth/device-management/device-management-table.component.ts @@ -15,6 +15,8 @@ import { import { DeviceDisplayData } from "./device-management.component"; /** Displays user devices in a sortable table view */ +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ standalone: true, selector: "auth-device-management-table", @@ -22,7 +24,11 @@ import { DeviceDisplayData } from "./device-management.component"; imports: [BadgeModule, ButtonModule, CommonModule, JslibModule, LinkModule, TableModule], }) export class DeviceManagementTableComponent implements OnChanges { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() devices: DeviceDisplayData[] = []; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onAuthRequestAnswered = new EventEmitter(); protected tableDataSource = new TableDataSource(); diff --git a/libs/angular/src/auth/device-management/device-management.component.ts b/libs/angular/src/auth/device-management/device-management.component.ts index 2c67812b586..d8f8cc10df4 100644 --- a/libs/angular/src/auth/device-management/device-management.component.ts +++ b/libs/angular/src/auth/device-management/device-management.component.ts @@ -50,6 +50,8 @@ export interface DeviceDisplayData { * - Medium to Large screens = `bit-table` view * - Small screens = `bit-item-group` view */ +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ standalone: true, selector: "auth-device-management", diff --git a/libs/angular/src/auth/environment-selector/environment-selector.component.ts b/libs/angular/src/auth/environment-selector/environment-selector.component.ts index 6fe3eaa92a0..89366f47b70 100644 --- a/libs/angular/src/auth/environment-selector/environment-selector.component.ts +++ b/libs/angular/src/auth/environment-selector/environment-selector.component.ts @@ -20,6 +20,8 @@ import { } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "environment-selector", templateUrl: "environment-selector.component.html", diff --git a/libs/angular/src/auth/guards/active-auth.guard.spec.ts b/libs/angular/src/auth/guards/active-auth.guard.spec.ts index de1bf40be11..d4d27626c11 100644 --- a/libs/angular/src/auth/guards/active-auth.guard.spec.ts +++ b/libs/angular/src/auth/guards/active-auth.guard.spec.ts @@ -13,6 +13,8 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { activeAuthGuard } from "./active-auth.guard"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: "", standalone: false }) class EmptyComponent {} diff --git a/libs/angular/src/auth/login-approval/login-approval-dialog.component.ts b/libs/angular/src/auth/login-approval/login-approval-dialog.component.ts index 19dc3f519c6..35333c43536 100644 --- a/libs/angular/src/auth/login-approval/login-approval-dialog.component.ts +++ b/libs/angular/src/auth/login-approval/login-approval-dialog.component.ts @@ -33,6 +33,8 @@ export interface LoginApprovalDialogParams { notificationId: string; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "login-approval-dialog.component.html", imports: [AsyncActionsModule, ButtonModule, CommonModule, DialogModule, JslibModule], diff --git a/libs/angular/src/auth/login-via-webauthn/login-via-webauthn.component.ts b/libs/angular/src/auth/login-via-webauthn/login-via-webauthn.component.ts index f795b66d916..fa2a01fe8e1 100644 --- a/libs/angular/src/auth/login-via-webauthn/login-via-webauthn.component.ts +++ b/libs/angular/src/auth/login-via-webauthn/login-via-webauthn.component.ts @@ -31,6 +31,8 @@ import { import { KeyService } from "@bitwarden/key-management"; export type State = "assert" | "assertFailed"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-login-via-webauthn", templateUrl: "login-via-webauthn.component.html", diff --git a/libs/angular/src/auth/password-management/change-password/change-password.component.ts b/libs/angular/src/auth/password-management/change-password/change-password.component.ts index 1512f348133..7f46ebfc9d4 100644 --- a/libs/angular/src/auth/password-management/change-password/change-password.component.ts +++ b/libs/angular/src/auth/password-management/change-password/change-password.component.ts @@ -39,12 +39,16 @@ import { ChangePasswordService } from "./change-password.service.abstraction"; * and by design to maintain a strong security posture as some flows could have the user * end up at a change password without having one before. */ +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "auth-change-password", templateUrl: "change-password.component.html", imports: [InputPasswordComponent, I18nPipe, CalloutComponent, CommonModule], }) export class ChangePasswordComponent implements OnInit { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() inputPasswordFlow: InputPasswordFlow = InputPasswordFlow.ChangePassword; activeAccount: Account | null = null; diff --git a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts index ff78952c562..805fe3c0173 100644 --- a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts +++ b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts @@ -45,6 +45,8 @@ import { SetInitialPasswordUserType, } from "./set-initial-password.service.abstraction"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ standalone: true, templateUrl: "set-initial-password.component.html", diff --git a/libs/auth/src/angular/fingerprint-dialog/fingerprint-dialog.component.ts b/libs/auth/src/angular/fingerprint-dialog/fingerprint-dialog.component.ts index 1769a57319c..6ef36a32448 100644 --- a/libs/auth/src/angular/fingerprint-dialog/fingerprint-dialog.component.ts +++ b/libs/auth/src/angular/fingerprint-dialog/fingerprint-dialog.component.ts @@ -9,6 +9,8 @@ export type FingerprintDialogData = { fingerprint: string[]; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "fingerprint-dialog.component.html", imports: [JslibModule, ButtonModule, DialogModule], diff --git a/libs/auth/src/angular/input-password/input-password.component.ts b/libs/auth/src/angular/input-password/input-password.component.ts index dda471c7129..019a9e3975e 100644 --- a/libs/auth/src/angular/input-password/input-password.component.ts +++ b/libs/auth/src/angular/input-password/input-password.component.ts @@ -99,6 +99,8 @@ interface InputPasswordForm { rotateUserKey?: FormControl; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "auth-input-password", templateUrl: "./input-password.component.html", @@ -118,24 +120,48 @@ interface InputPasswordForm { ], }) export class InputPasswordComponent implements OnInit { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild(PasswordStrengthV2Component) passwordStrengthComponent: | PasswordStrengthV2Component | undefined = undefined; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onPasswordFormSubmit = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onSecondaryButtonClick = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() isSubmitting = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) flow!: InputPasswordFlow; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ transform: (val: string) => val?.trim().toLowerCase() }) email?: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() userId?: UserId; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() loading = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() masterPasswordPolicyOptions?: MasterPasswordPolicyOptions; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() inlineButtons = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() primaryButtonText?: Translation; protected primaryButtonTextStr: string = ""; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() secondaryButtonText?: Translation; protected secondaryButtonTextStr: string = ""; diff --git a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts index a2018817fed..26293285008 100644 --- a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts +++ b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts @@ -50,6 +50,8 @@ enum State { ExistingUserUntrustedDevice, } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "./login-decryption-options.component.html", imports: [ diff --git a/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts b/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts index 10b19567946..2436593dfda 100644 --- a/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts +++ b/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts @@ -56,6 +56,8 @@ const matchOptions: IsActiveMatchOptions = { matrixParams: "ignored", }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "./login-via-auth-request.component.html", imports: [ButtonModule, CommonModule, JslibModule, LinkModule, RouterModule], diff --git a/libs/auth/src/angular/login/login-secondary-content.component.ts b/libs/auth/src/angular/login/login-secondary-content.component.ts index 9cd4cfd2502..29474d5c8c6 100644 --- a/libs/auth/src/angular/login/login-secondary-content.component.ts +++ b/libs/auth/src/angular/login/login-secondary-content.component.ts @@ -8,6 +8,8 @@ import { DefaultServerSettingsService } from "@bitwarden/common/platform/service // eslint-disable-next-line no-restricted-imports import { LinkModule } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ imports: [CommonModule, JslibModule, LinkModule, RouterModule], template: ` diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts index 9ade2c1d0af..537a42700c8 100644 --- a/libs/auth/src/angular/login/login.component.ts +++ b/libs/auth/src/angular/login/login.component.ts @@ -67,6 +67,8 @@ export enum LoginUiState { MASTER_PASSWORD_ENTRY = "MasterPasswordEntry", } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "./login.component.html", imports: [ @@ -83,6 +85,8 @@ export enum LoginUiState { ], }) export class LoginComponent implements OnInit, OnDestroy { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("masterPasswordInputRef") masterPasswordInputRef: ElementRef | undefined; private destroy$ = new Subject(); diff --git a/libs/auth/src/angular/new-device-verification/new-device-verification.component.ts b/libs/auth/src/angular/new-device-verification/new-device-verification.component.ts index 2211b3390a7..c3d6ff5d1fe 100644 --- a/libs/auth/src/angular/new-device-verification/new-device-verification.component.ts +++ b/libs/auth/src/angular/new-device-verification/new-device-verification.component.ts @@ -30,6 +30,8 @@ import { NewDeviceVerificationComponentService } from "./new-device-verification /** * Component for verifying a new device via a one-time password (OTP). */ +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-new-device-verification", templateUrl: "./new-device-verification.component.html", diff --git a/libs/auth/src/angular/password-callout/password-callout.component.ts b/libs/auth/src/angular/password-callout/password-callout.component.ts index 7a28700f109..2d97c33c50b 100644 --- a/libs/auth/src/angular/password-callout/password-callout.component.ts +++ b/libs/auth/src/angular/password-callout/password-callout.component.ts @@ -10,13 +10,19 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic // eslint-disable-next-line no-restricted-imports import { CalloutModule } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "auth-password-callout", templateUrl: "password-callout.component.html", imports: [CommonModule, JslibModule, CalloutModule], }) export class PasswordCalloutComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() message = "masterPasswordPolicyInEffect"; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() policy: MasterPasswordPolicyOptions; constructor(private i18nService: I18nService) {} diff --git a/libs/auth/src/angular/password-hint/password-hint.component.ts b/libs/auth/src/angular/password-hint/password-hint.component.ts index 3189bf8f187..50d53e00ad3 100644 --- a/libs/auth/src/angular/password-hint/password-hint.component.ts +++ b/libs/auth/src/angular/password-hint/password-hint.component.ts @@ -22,6 +22,8 @@ import { ToastService, } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "./password-hint.component.html", imports: [ diff --git a/libs/auth/src/angular/registration/registration-env-selector/registration-env-selector.component.ts b/libs/auth/src/angular/registration/registration-env-selector/registration-env-selector.component.ts index 93ddd00fdd6..d2dbd72989d 100644 --- a/libs/auth/src/angular/registration/registration-env-selector/registration-env-selector.component.ts +++ b/libs/auth/src/angular/registration/registration-env-selector/registration-env-selector.component.ts @@ -25,12 +25,16 @@ import { SelfHostedEnvConfigDialogComponent } from "../../self-hosted-env-config * Component for selecting the environment to register with in the email verification registration flow. * Outputs the selected region to the parent component so it can respond as necessary. */ +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "auth-registration-env-selector", templateUrl: "registration-env-selector.component.html", imports: [CommonModule, JslibModule, ReactiveFormsModule, FormFieldModule, SelectModule], }) export class RegistrationEnvSelectorComponent implements OnInit, OnDestroy { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() selectedRegionChange = new EventEmitter(); ServerEnvironmentType = Region; diff --git a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts index 952b9e9ce75..7e7b9131fac 100644 --- a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts +++ b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts @@ -31,6 +31,8 @@ import { PasswordInputResult } from "../../input-password/password-input-result" import { RegistrationFinishService } from "./registration-finish.service"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "auth-registration-finish", templateUrl: "./registration-finish.component.html", diff --git a/libs/auth/src/angular/registration/registration-link-expired/registration-link-expired.component.ts b/libs/auth/src/angular/registration/registration-link-expired/registration-link-expired.component.ts index 9e75a8b888c..e7a3e99759c 100644 --- a/libs/auth/src/angular/registration/registration-link-expired/registration-link-expired.component.ts +++ b/libs/auth/src/angular/registration/registration-link-expired/registration-link-expired.component.ts @@ -19,6 +19,8 @@ export interface RegistrationLinkExpiredComponentData { loginRoute: string; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "auth-registration-link-expired", templateUrl: "./registration-link-expired.component.html", diff --git a/libs/auth/src/angular/registration/registration-start/registration-start-secondary.component.ts b/libs/auth/src/angular/registration/registration-start/registration-start-secondary.component.ts index f30dc8a3822..31e9cbc6316 100644 --- a/libs/auth/src/angular/registration/registration-start/registration-start-secondary.component.ts +++ b/libs/auth/src/angular/registration/registration-start/registration-start-secondary.component.ts @@ -18,6 +18,8 @@ export interface RegistrationStartSecondaryComponentData { loginRoute: string; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "auth-registration-start-secondary", templateUrl: "./registration-start-secondary.component.html", diff --git a/libs/auth/src/angular/registration/registration-start/registration-start.component.ts b/libs/auth/src/angular/registration/registration-start/registration-start.component.ts index 16018cecfa7..714f6d49342 100644 --- a/libs/auth/src/angular/registration/registration-start/registration-start.component.ts +++ b/libs/auth/src/angular/registration/registration-start/registration-start.component.ts @@ -40,6 +40,8 @@ const DEFAULT_MARKETING_EMAILS_PREF_BY_REGION: Record = { [Region.SelfHosted]: false, }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "auth-registration-start", templateUrl: "./registration-start.component.html", @@ -57,6 +59,8 @@ const DEFAULT_MARKETING_EMAILS_PREF_BY_REGION: Record = { ], }) export class RegistrationStartComponent implements OnInit, OnDestroy { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() registrationStartStateChange = new EventEmitter(); state: RegistrationStartState = RegistrationStartState.USER_DATA_ENTRY; diff --git a/libs/auth/src/angular/self-hosted-env-config-dialog/self-hosted-env-config-dialog.component.ts b/libs/auth/src/angular/self-hosted-env-config-dialog/self-hosted-env-config-dialog.component.ts index 16c25f2404f..40fdfb8c17c 100644 --- a/libs/auth/src/angular/self-hosted-env-config-dialog/self-hosted-env-config-dialog.component.ts +++ b/libs/auth/src/angular/self-hosted-env-config-dialog/self-hosted-env-config-dialog.component.ts @@ -54,6 +54,8 @@ function selfHostedEnvSettingsFormValidator(): ValidatorFn { /** * Dialog for configuring self-hosted environment settings. */ +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "self-hosted-env-config-dialog", templateUrl: "self-hosted-env-config-dialog.component.html", diff --git a/libs/auth/src/angular/sso/sso.component.ts b/libs/auth/src/angular/sso/sso.component.ts index 8636595759e..0b6bb1159f4 100644 --- a/libs/auth/src/angular/sso/sso.component.ts +++ b/libs/auth/src/angular/sso/sso.component.ts @@ -62,6 +62,8 @@ interface QueryParams { /** * This component handles the SSO flow. */ +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "sso.component.html", imports: [ diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-authenticator/two-factor-auth-authenticator.component.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-authenticator/two-factor-auth-authenticator.component.ts index c53bffe2496..2bc5d1a0381 100644 --- a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-authenticator/two-factor-auth-authenticator.component.ts +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-authenticator/two-factor-auth-authenticator.component.ts @@ -14,6 +14,8 @@ import { AsyncActionsModule, } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-two-factor-auth-authenticator", templateUrl: "two-factor-auth-authenticator.component.html", @@ -32,7 +34,11 @@ import { providers: [], }) export class TwoFactorAuthAuthenticatorComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) tokenFormControl: FormControl | undefined = undefined; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() tokenChange = new EventEmitter<{ token: string }>(); onTokenChange(event: Event) { diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-duo/two-factor-auth-duo.component.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-duo/two-factor-auth-duo.component.ts index 5ad70d3792d..0e089c674be 100644 --- a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-duo/two-factor-auth-duo.component.ts +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-duo/two-factor-auth-duo.component.ts @@ -25,6 +25,8 @@ import { TwoFactorAuthDuoComponentService, } from "./two-factor-auth-duo-component.service"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-two-factor-auth-duo", template: "", @@ -43,7 +45,11 @@ import { providers: [], }) export class TwoFactorAuthDuoComponent implements OnInit { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() tokenEmitter = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() providerData: any; duoFramelessUrl: string | undefined = undefined; diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-component-cache.service.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-component-cache.service.ts index d98387e1cf5..79a7e1f9ddc 100644 --- a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-component-cache.service.ts +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-component-cache.service.ts @@ -36,7 +36,7 @@ export class TwoFactorAuthEmailComponentCacheService { /** * Signal for the cached email state. */ - private emailCache: WritableSignal = + private readonly emailCache: WritableSignal = this.viewCacheService.signal({ key: TWO_FACTOR_AUTH_EMAIL_COMPONENT_CACHE_KEY, initialValue: null, diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.ts index 084e8e6e851..000d391b62c 100644 --- a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.ts +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.ts @@ -26,6 +26,8 @@ import { import { TwoFactorAuthEmailComponentCacheService } from "./two-factor-auth-email-component-cache.service"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-two-factor-auth-email", templateUrl: "two-factor-auth-email.component.html", @@ -49,7 +51,11 @@ import { TwoFactorAuthEmailComponentCacheService } from "./two-factor-auth-email ], }) export class TwoFactorAuthEmailComponent implements OnInit { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) tokenFormControl: FormControl | undefined = undefined; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() tokenChange = new EventEmitter<{ token: string }>(); twoFactorEmail: string | undefined = undefined; diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-webauthn/two-factor-auth-webauthn.component.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-webauthn/two-factor-auth-webauthn.component.ts index 710d5dc4de0..71a91ec20e7 100644 --- a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-webauthn/two-factor-auth-webauthn.component.ts +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-webauthn/two-factor-auth-webauthn.component.ts @@ -32,6 +32,8 @@ export interface WebAuthnResult { remember?: boolean; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-two-factor-auth-webauthn", templateUrl: "two-factor-auth-webauthn.component.html", @@ -50,7 +52,11 @@ export interface WebAuthnResult { providers: [], }) export class TwoFactorAuthWebAuthnComponent implements OnInit, OnDestroy { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() webAuthnResultEmitter = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() webAuthnInNewTabEmitter = new EventEmitter(); webAuthnReady = false; diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-yubikey/two-factor-auth-yubikey.component.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-yubikey/two-factor-auth-yubikey.component.ts index 7218bee056c..40bd3fb551d 100644 --- a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-yubikey/two-factor-auth-yubikey.component.ts +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-yubikey/two-factor-auth-yubikey.component.ts @@ -14,6 +14,8 @@ import { AsyncActionsModule, } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-two-factor-auth-yubikey", templateUrl: "two-factor-auth-yubikey.component.html", @@ -32,5 +34,7 @@ import { providers: [], }) export class TwoFactorAuthYubikeyComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) tokenFormControl: FormControl | undefined = undefined; } diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth-component-cache.service.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth-component-cache.service.ts index 33aa76680e4..89fb3d34e96 100644 --- a/libs/auth/src/angular/two-factor-auth/two-factor-auth-component-cache.service.ts +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth-component-cache.service.ts @@ -42,7 +42,7 @@ export class TwoFactorAuthComponentCacheService { /** * Signal for the cached TwoFactorAuthData. */ - private twoFactorAuthComponentCache: WritableSignal = + private readonly twoFactorAuthComponentCache: WritableSignal = this.viewCacheService.signal({ key: TWO_FACTOR_AUTH_COMPONENT_CACHE_KEY, initialValue: null, diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts index 9418030d7a1..5d36fd384ca 100644 --- a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts @@ -46,6 +46,8 @@ import { TwoFactorAuthComponentCacheService } from "./two-factor-auth-component- import { TwoFactorAuthComponentService } from "./two-factor-auth-component.service"; import { TwoFactorAuthComponent } from "./two-factor-auth.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ standalone: false }) class TestTwoFactorComponent extends TwoFactorAuthComponent {} diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts index 4c0784928d4..52f20b04601 100644 --- a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts @@ -75,6 +75,8 @@ import { TwoFactorOptionsDialogResult, } from "./two-factor-options.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-two-factor-auth", templateUrl: "two-factor-auth.component.html", @@ -99,6 +101,8 @@ import { ], }) export class TwoFactorAuthComponent implements OnInit, OnDestroy { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("continueButton", { read: ElementRef, static: false }) continueButton: | ElementRef | undefined = undefined; @@ -114,6 +118,8 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { twoFactorProviders: Map | null = null; selectedProviderData: { [key: string]: string } | undefined; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("duoComponent") duoComponent!: TwoFactorAuthDuoComponent; form = this.formBuilder.group({ diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth.guard.spec.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth.guard.spec.ts index 06b998c5725..116da73173f 100644 --- a/libs/auth/src/angular/two-factor-auth/two-factor-auth.guard.spec.ts +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.guard.spec.ts @@ -11,6 +11,8 @@ import { LoginStrategyServiceAbstraction } from "../../common"; import { TwoFactorAuthGuard } from "./two-factor-auth.guard"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: "", standalone: true }) export class EmptyComponent {} diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-options.component.ts b/libs/auth/src/angular/two-factor-auth/two-factor-options.component.ts index 7e7e02e5e5a..d0ad9be6103 100644 --- a/libs/auth/src/angular/two-factor-auth/two-factor-options.component.ts +++ b/libs/auth/src/angular/two-factor-auth/two-factor-options.component.ts @@ -30,6 +30,8 @@ export type TwoFactorOptionsDialogResult = { type: TwoFactorProviderType; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-two-factor-options", templateUrl: "two-factor-options.component.html", diff --git a/libs/auth/src/angular/user-verification/user-verification-dialog.component.ts b/libs/auth/src/angular/user-verification/user-verification-dialog.component.ts index 4dfb7a6a995..09d428d4ba7 100644 --- a/libs/auth/src/angular/user-verification/user-verification-dialog.component.ts +++ b/libs/auth/src/angular/user-verification/user-verification-dialog.component.ts @@ -30,6 +30,8 @@ import { } from "./user-verification-dialog.types"; import { UserVerificationFormInputComponent } from "./user-verification-form-input.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "user-verification-dialog.component.html", imports: [ diff --git a/libs/auth/src/angular/user-verification/user-verification-form-input.component.ts b/libs/auth/src/angular/user-verification/user-verification-form-input.component.ts index e1b8207d970..296359c92ff 100644 --- a/libs/auth/src/angular/user-verification/user-verification-form-input.component.ts +++ b/libs/auth/src/angular/user-verification/user-verification-form-input.component.ts @@ -40,6 +40,8 @@ import { ActiveClientVerificationOption } from "./active-client-verification-opt * This is exposed to the parent component via the ControlValueAccessor interface (e.g. bind it to a FormControl). * Use UserVerificationService to verify the user's input. */ +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-user-verification-form-input", templateUrl: "user-verification-form-input.component.html", @@ -69,8 +71,12 @@ import { ActiveClientVerificationOption } from "./active-client-verification-opt ], }) export class UserVerificationFormInputComponent implements ControlValueAccessor, OnInit, OnDestroy { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() verificationType: "server" | "client" = "server"; // server represents original behavior private _invalidSecret = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() get invalidSecret() { return this._invalidSecret; @@ -88,11 +94,17 @@ export class UserVerificationFormInputComponent implements ControlValueAccessor, } this.secret.updateValueAndValidity({ emitEvent: false }); } + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() invalidSecretChange = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() activeClientVerificationOptionChange = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() biometricsVerificationResultChange = new EventEmitter(); readonly Icons = { UserVerificationBiometricsIcon }; diff --git a/libs/auth/src/angular/vault-timeout-input/vault-timeout-input.component.ts b/libs/auth/src/angular/vault-timeout-input/vault-timeout-input.component.ts index b5d87c60882..2335de34c21 100644 --- a/libs/auth/src/angular/vault-timeout-input/vault-timeout-input.component.ts +++ b/libs/auth/src/angular/vault-timeout-input/vault-timeout-input.component.ts @@ -44,6 +44,8 @@ type VaultTimeoutForm = FormGroup<{ type VaultTimeoutFormValue = VaultTimeoutForm["value"]; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "auth-vault-timeout-input", templateUrl: "vault-timeout-input.component.html", @@ -110,6 +112,8 @@ export class VaultTimeoutInputComponent }), }); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() vaultTimeoutOptions: VaultTimeoutOption[]; vaultTimeoutPolicy: Policy; diff --git a/libs/auth/src/common/services/auth-request/default-login-via-auth-request-cache.service.ts b/libs/auth/src/common/services/auth-request/default-login-via-auth-request-cache.service.ts index 80dbafd3159..8b947c41c46 100644 --- a/libs/auth/src/common/services/auth-request/default-login-via-auth-request-cache.service.ts +++ b/libs/auth/src/common/services/auth-request/default-login-via-auth-request-cache.service.ts @@ -16,7 +16,7 @@ const LOGIN_VIA_AUTH_CACHE_KEY = "login-via-auth-request-form-cache"; export class LoginViaAuthRequestCacheService { private viewCacheService: ViewCacheService = inject(ViewCacheService); - private defaultLoginViaAuthRequestCache: WritableSignal = + private readonly defaultLoginViaAuthRequestCache: WritableSignal = this.viewCacheService.signal({ key: LOGIN_VIA_AUTH_CACHE_KEY, initialValue: null, From 7e7107f16500892b424e4ced178aaa0c8bd29f9a Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 23 Oct 2025 11:38:10 +0200 Subject: [PATCH 05/73] [PM-27221] Update legacy kdf state on master password unlock sync (#16966) * Update legacy kdf state on master password unlock sync * Fix cli build * Fix * Fix build * Fix cli * Fix browser --- apps/browser/src/background/main.background.ts | 1 + apps/cli/src/service-container/service-container.ts | 1 + libs/angular/src/services/jslib-services.module.ts | 1 + libs/common/src/platform/sync/default-sync.service.spec.ts | 5 ++++- libs/common/src/platform/sync/default-sync.service.ts | 4 +++- 5 files changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 7a4ee64070f..8170c2a65a0 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1050,6 +1050,7 @@ export default class MainBackground { this.authService, this.stateProvider, this.securityStateService, + this.kdfConfigService, ); this.syncServiceListener = new SyncServiceListener( diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index d10849bae0f..3c4ee55361f 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -846,6 +846,7 @@ export class ServiceContainer { this.authService, this.stateProvider, this.securityStateService, + this.kdfConfigService, ); this.totpService = new TotpService(this.sdkService); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 5d2a23444f0..47e9e7d23bd 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -847,6 +847,7 @@ const safeProviders: SafeProvider[] = [ AuthServiceAbstraction, StateProvider, SecurityStateService, + KdfConfigService, ], }), safeProvider({ diff --git a/libs/common/src/platform/sync/default-sync.service.spec.ts b/libs/common/src/platform/sync/default-sync.service.spec.ts index f60b42ce450..621033ced65 100644 --- a/libs/common/src/platform/sync/default-sync.service.spec.ts +++ b/libs/common/src/platform/sync/default-sync.service.spec.ts @@ -15,7 +15,7 @@ import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-st import { SecurityStateService } from "@bitwarden/common/key-management/security-state/abstractions/security-state.service"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports -import { KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management"; +import { KdfConfigService, KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management"; import { Matrix } from "../../../spec/matrix"; import { ApiService } from "../../abstractions/api.service"; @@ -75,6 +75,7 @@ describe("DefaultSyncService", () => { let authService: MockProxy; let stateProvider: MockProxy; let securityStateService: MockProxy; + let kdfConfigService: MockProxy; let sut: DefaultSyncService; @@ -105,6 +106,7 @@ describe("DefaultSyncService", () => { authService = mock(); stateProvider = mock(); securityStateService = mock(); + kdfConfigService = mock(); sut = new DefaultSyncService( masterPasswordAbstraction, @@ -132,6 +134,7 @@ describe("DefaultSyncService", () => { authService, stateProvider, securityStateService, + kdfConfigService, ); }); diff --git a/libs/common/src/platform/sync/default-sync.service.ts b/libs/common/src/platform/sync/default-sync.service.ts index d5fa2d0ae68..e599fbc1c48 100644 --- a/libs/common/src/platform/sync/default-sync.service.ts +++ b/libs/common/src/platform/sync/default-sync.service.ts @@ -12,7 +12,7 @@ import { // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. import { SecurityStateService } from "@bitwarden/common/key-management/security-state/abstractions/security-state.service"; // eslint-disable-next-line no-restricted-imports -import { KeyService } from "@bitwarden/key-management"; +import { KdfConfigService, KeyService } from "@bitwarden/key-management"; // FIXME: remove `src` and fix import // eslint-disable-next-line no-restricted-imports @@ -100,6 +100,7 @@ export class DefaultSyncService extends CoreSyncService { authService: AuthService, stateProvider: StateProvider, private securityStateService: SecurityStateService, + private kdfConfigService: KdfConfigService, ) { super( tokenService, @@ -434,6 +435,7 @@ export class DefaultSyncService extends CoreSyncService { masterPasswordUnlockData, userId, ); + await this.kdfConfigService.setKdfConfig(userId, masterPasswordUnlockData.kdf); } } } From 7f86f2d0ac69fb3d952a2cbfc95e1b8fd75d302e Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 23 Oct 2025 14:04:25 +0200 Subject: [PATCH 06/73] [PM-26340] Implement encrypted memory store (#16659) * Extract windows biometrics v2 changes Co-authored-by: Bernd Schoolmann * Address some code review feedback * cargo fmt * rely on zeroizing allocator * Handle TDE edge cases * Update windows default * Make windows rust code async and fix restoring focus freezes * fix formatting * cleanup native logging * Add unit test coverage * Add missing logic to edge case for PIN disable. * Address code review feedback * fix test * code review changes * fix clippy warning * Swap to unimplemented on each method * Implement encrypted memory store * Make dpapi secure key container pub(super) * Add comments on sync and send * Clean up comments * Clean up * Fix build * Add logging and update codeowners * Run cargo fmt * Clean up doc * fix unit tests * Update apps/desktop/desktop_native/core/src/secure_memory/secure_key/mod.rs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Handle tampering with re-key and log * Add docs * Fix windows build * Prevent rust flycheck log from being commited to git * Undo feature flag change * Add env var override and docs * Add deps to km owership --------- Co-authored-by: Thomas Avery Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> --- .github/CODEOWNERS | 1 + .github/renovate.json5 | 11 +- .gitignore | 1 + apps/desktop/desktop_native/Cargo.lock | 93 ++++++- apps/desktop/desktop_native/Cargo.toml | 2 + apps/desktop/desktop_native/core/Cargo.toml | 2 + .../core/src/secure_memory/dpapi.rs | 2 +- .../secure_memory/encrypted_memory_store.rs | 105 ++++++++ .../core/src/secure_memory/mod.rs | 7 +- .../src/secure_memory/secure_key/crypto.rs | 96 +++++++ .../src/secure_memory/secure_key/dpapi.rs | 93 +++++++ .../src/secure_memory/secure_key/keyctl.rs | 100 ++++++++ .../secure_memory/secure_key/memfd_secret.rs | 109 ++++++++ .../src/secure_memory/secure_key/mlock.rs | 83 ++++++ .../core/src/secure_memory/secure_key/mod.rs | 242 ++++++++++++++++++ 15 files changed, 942 insertions(+), 5 deletions(-) create mode 100644 apps/desktop/desktop_native/core/src/secure_memory/encrypted_memory_store.rs create mode 100644 apps/desktop/desktop_native/core/src/secure_memory/secure_key/crypto.rs create mode 100644 apps/desktop/desktop_native/core/src/secure_memory/secure_key/dpapi.rs create mode 100644 apps/desktop/desktop_native/core/src/secure_memory/secure_key/keyctl.rs create mode 100644 apps/desktop/desktop_native/core/src/secure_memory/secure_key/memfd_secret.rs create mode 100644 apps/desktop/desktop_native/core/src/secure_memory/secure_key/mlock.rs create mode 100644 apps/desktop/desktop_native/core/src/secure_memory/secure_key/mod.rs diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ae5d62a90c2..f03cf3ee2a8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,6 +8,7 @@ apps/desktop/desktop_native @bitwarden/team-platform-dev apps/desktop/desktop_native/objc/src/native/autofill @bitwarden/team-autofill-desktop-dev apps/desktop/desktop_native/core/src/autofill @bitwarden/team-autofill-desktop-dev +apps/desktop/desktop_native/core/src/secure_memory @bitwarden/team-key-management-dev ## No ownership for Cargo.lock and Cargo.toml to allow dependency updates apps/desktop/desktop_native/Cargo.lock apps/desktop/desktop_native/Cargo.toml diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 6fcffb1f875..f898df460c9 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -400,7 +400,16 @@ reviewers: ["team:team-vault-dev"], }, { - matchPackageNames: ["aes", "big-integer", "cbc", "rsa", "russh-cryptovec", "sha2"], + matchPackageNames: [ + "aes", + "big-integer", + "cbc", + "rsa", + "russh-cryptovec", + "sha2", + "memsec", + "linux-keyutils", + ], description: "Key Management owned dependencies", commitMessagePrefix: "[deps] KM:", reviewers: ["team:team-key-management-dev"], diff --git a/.gitignore b/.gitignore index 61a20195592..6b13d22caa7 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ npm-debug.log # Build directories dist build +target .angular/cache .flatpak .flatpak-repo diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 0aa040bbcf1..3df6b41734b 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -927,6 +927,8 @@ dependencies = [ "interprocess", "keytar", "libc", + "linux-keyutils", + "memsec", "oo7", "pin-project", "pkcs8", @@ -1793,6 +1795,16 @@ dependencies = [ "cc", ] +[[package]] +name = "linux-keyutils" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "761e49ec5fd8a5a463f9b84e877c373d888935b71c6be78f3767fe2ae6bed18e" +dependencies = [ + "bitflags", + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -1878,6 +1890,17 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memsec" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c797b9d6bb23aab2fc369c65f871be49214f5c759af65bde26ffaaa2b646b492" +dependencies = [ + "getrandom 0.2.16", + "libc", + "windows-sys 0.45.0", +] + [[package]] name = "mime" version = "0.3.17" @@ -3993,6 +4016,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -4020,6 +4052,21 @@ dependencies = [ "windows-targets 0.53.3", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -4068,6 +4115,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -4086,6 +4139,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -4104,6 +4163,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -4134,6 +4199,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -4161,6 +4232,12 @@ dependencies = [ "windows-core 0.61.0", ] +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -4179,6 +4256,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -4197,6 +4280,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -4416,9 +4505,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" dependencies = [ "zeroize_derive", ] diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index 855b0b3aa43..edf3cb44eca 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -39,7 +39,9 @@ homedir = "=0.3.4" interprocess = "=2.2.1" keytar = "=0.1.6" libc = "=0.2.172" +linux-keyutils = "=0.2.4" log = "=0.4.25" +memsec = "=0.7.0" napi = "=2.16.17" napi-build = "=2.2.0" napi-derive = "=2.16.13" diff --git a/apps/desktop/desktop_native/core/Cargo.toml b/apps/desktop/desktop_native/core/Cargo.toml index b7e4c9b7a83..f6c9d669df6 100644 --- a/apps/desktop/desktop_native/core/Cargo.toml +++ b/apps/desktop/desktop_native/core/Cargo.toml @@ -32,6 +32,7 @@ ed25519 = { workspace = true, features = ["pkcs8"] } futures = { workspace = true } homedir = { workspace = true } interprocess = { workspace = true, features = ["tokio"] } +memsec = { workspace = true, features = ["alloc_ext"] } pin-project = { workspace = true } pkcs8 = { workspace = true, features = ["alloc", "encryption", "pem"] } rand = { workspace = true } @@ -87,6 +88,7 @@ desktop_objc = { path = "../objc" } [target.'cfg(target_os = "linux")'.dependencies] oo7 = { workspace = true } libc = { workspace = true } +linux-keyutils = { workspace = true } ashpd = { workspace = true } zbus = { workspace = true, optional = true } diff --git a/apps/desktop/desktop_native/core/src/secure_memory/dpapi.rs b/apps/desktop/desktop_native/core/src/secure_memory/dpapi.rs index ca9b6081d69..3ff8a6d3d83 100644 --- a/apps/desktop/desktop_native/core/src/secure_memory/dpapi.rs +++ b/apps/desktop/desktop_native/core/src/secure_memory/dpapi.rs @@ -54,7 +54,7 @@ impl SecureMemoryStore for DpapiSecretKVStore { self.map.insert(key, padded_data); } - fn get(&self, key: &str) -> Option> { + fn get(&mut self, key: &str) -> Option> { self.map.get(key).map(|data| { // A copy is created, that is then mutated by the DPAPI unprotect function. let mut data = data.clone(); diff --git a/apps/desktop/desktop_native/core/src/secure_memory/encrypted_memory_store.rs b/apps/desktop/desktop_native/core/src/secure_memory/encrypted_memory_store.rs new file mode 100644 index 00000000000..a8952d8f55a --- /dev/null +++ b/apps/desktop/desktop_native/core/src/secure_memory/encrypted_memory_store.rs @@ -0,0 +1,105 @@ +use tracing::error; + +use crate::secure_memory::{ + secure_key::{EncryptedMemory, SecureMemoryEncryptionKey}, + SecureMemoryStore, +}; + +/// An encrypted memory store holds a platform protected symmetric encryption key, and uses it +/// to encrypt all items it stores. The ciphertexts for the items are not specially protected. This +/// allows circumventing length and amount limitations on platform specific secure memory APIs since +/// only a single short item needs to be protected. +/// +/// The key is briefly in process memory during encryption and decryption, in memory that is protected +/// from swapping to disk via mlock, and then zeroed out immediately after use. +#[allow(unused)] +pub(crate) struct EncryptedMemoryStore { + map: std::collections::HashMap, + memory_encryption_key: SecureMemoryEncryptionKey, +} + +impl EncryptedMemoryStore { + #[allow(unused)] + pub(crate) fn new() -> Self { + EncryptedMemoryStore { + map: std::collections::HashMap::new(), + memory_encryption_key: SecureMemoryEncryptionKey::new(), + } + } +} + +impl SecureMemoryStore for EncryptedMemoryStore { + fn put(&mut self, key: String, value: &[u8]) { + let encrypted_value = self.memory_encryption_key.encrypt(value); + self.map.insert(key, encrypted_value); + } + + fn get(&mut self, key: &str) -> Option> { + let encrypted_memory = self.map.get(key); + if let Some(encrypted_memory) = encrypted_memory { + match self.memory_encryption_key.decrypt(encrypted_memory) { + Ok(plaintext) => Some(plaintext), + Err(_) => { + error!("In memory store, decryption failed for key {}. The memory may have been tampered with. re-keying.", key); + self.memory_encryption_key = SecureMemoryEncryptionKey::new(); + self.clear(); + None + } + } + } else { + None + } + } + + fn has(&self, key: &str) -> bool { + self.map.contains_key(key) + } + + fn remove(&mut self, key: &str) { + self.map.remove(key); + } + + fn clear(&mut self) { + self.map.clear(); + } +} + +impl Drop for EncryptedMemoryStore { + fn drop(&mut self) { + self.clear(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_secret_kv_store_various_sizes() { + let mut store = EncryptedMemoryStore::new(); + for size in 0..=2048 { + let key = format!("test_key_{}", size); + let value: Vec = (0..size).map(|i| (i % 256) as u8).collect(); + store.put(key.clone(), &value); + assert!(store.has(&key), "Store should have key for size {}", size); + assert_eq!( + store.get(&key), + Some(value), + "Value mismatch for size {}", + size + ); + } + } + + #[test] + fn test_crud() { + let mut store = EncryptedMemoryStore::new(); + let key = "test_key".to_string(); + let value = vec![1, 2, 3, 4, 5]; + store.put(key.clone(), &value); + assert!(store.has(&key)); + assert_eq!(store.get(&key), Some(value)); + store.remove(&key); + assert!(!store.has(&key)); + } +} diff --git a/apps/desktop/desktop_native/core/src/secure_memory/mod.rs b/apps/desktop/desktop_native/core/src/secure_memory/mod.rs index 0cb604e03f2..8695904758e 100644 --- a/apps/desktop/desktop_native/core/src/secure_memory/mod.rs +++ b/apps/desktop/desktop_native/core/src/secure_memory/mod.rs @@ -1,6 +1,9 @@ #[cfg(target_os = "windows")] pub(crate) mod dpapi; +mod encrypted_memory_store; +mod secure_key; + /// The secure memory store provides an ephemeral key-value store for sensitive data. /// Data stored in this store is prevented from being swapped to disk and zeroed out. Additionally, /// platform-specific protections are applied to prevent memory dumps or debugger access from @@ -12,7 +15,9 @@ pub(crate) trait SecureMemoryStore { /// Retrieves a copy of the value associated with the given key from secure memory. /// This copy does not have additional memory protections applied, and should be zeroed when no /// longer needed. - fn get(&self, key: &str) -> Option>; + /// + /// Note: If memory was tampered with, this will re-key the store and return None. + fn get(&mut self, key: &str) -> Option>; /// Checks if a value is stored under the given key. fn has(&self, key: &str) -> bool; /// Removes the value associated with the given key from secure memory. diff --git a/apps/desktop/desktop_native/core/src/secure_memory/secure_key/crypto.rs b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/crypto.rs new file mode 100644 index 00000000000..1ee6c4cdf40 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/crypto.rs @@ -0,0 +1,96 @@ +use std::ptr::NonNull; + +use chacha20poly1305::{aead::Aead, Key, KeyInit}; +use rand::{rng, Rng}; + +pub(super) const KEY_SIZE: usize = 32; +pub(super) const NONCE_SIZE: usize = 24; + +/// The encryption performed here is xchacha-poly1305. Any tampering with the key or the ciphertexts will result +/// in a decryption failure and panic. The key's memory contents are protected from being swapped to disk +/// via mlock. +pub(super) struct MemoryEncryptionKey(NonNull<[u8]>); + +/// An encrypted memory blob that must be decrypted using the same key that it was encrypted with. +pub struct EncryptedMemory { + nonce: [u8; NONCE_SIZE], + ciphertext: Vec, +} + +impl MemoryEncryptionKey { + pub fn new() -> Self { + let mut key = [0u8; KEY_SIZE]; + rng().fill(&mut key); + MemoryEncryptionKey::from(&key) + } + + /// Encrypts the given plaintext using the key. + #[allow(unused)] + pub(super) fn encrypt(&self, plaintext: &[u8]) -> EncryptedMemory { + let cipher = chacha20poly1305::XChaCha20Poly1305::new(Key::from_slice(self.as_ref())); + let mut nonce = [0u8; NONCE_SIZE]; + rng().fill(&mut nonce); + let ciphertext = cipher + .encrypt(chacha20poly1305::XNonce::from_slice(&nonce), plaintext) + .expect("encryption should not fail"); + EncryptedMemory { nonce, ciphertext } + } + + /// Decrypts the given encrypted memory using the key. A decryption failure will panic. This is + /// okay because neither the keys nor ciphertexts should ever fail to decrypt, and doing so + /// indicates that the process memory was tampered with. + #[allow(unused)] + pub(super) fn decrypt(&self, encrypted: &EncryptedMemory) -> Result, DecryptionError> { + let cipher = chacha20poly1305::XChaCha20Poly1305::new(Key::from_slice(self.as_ref())); + cipher + .decrypt( + chacha20poly1305::XNonce::from_slice(&encrypted.nonce), + encrypted.ciphertext.as_ref(), + ) + .map_err(|_| DecryptionError::CouldNotDecrypt) + } +} + +impl Drop for MemoryEncryptionKey { + fn drop(&mut self) { + unsafe { + memsec::free(self.0); + } + } +} + +impl From<&[u8; KEY_SIZE]> for MemoryEncryptionKey { + fn from(value: &[u8; KEY_SIZE]) -> Self { + let mut ptr: NonNull<[u8]> = + unsafe { memsec::malloc_sized(KEY_SIZE).expect("malloc_sized should work") }; + unsafe { + std::ptr::copy_nonoverlapping(value.as_ptr(), ptr.as_mut().as_mut_ptr(), KEY_SIZE); + } + MemoryEncryptionKey(ptr) + } +} + +impl AsRef<[u8]> for MemoryEncryptionKey { + fn as_ref(&self) -> &[u8] { + unsafe { self.0.as_ref() } + } +} + +#[derive(Debug)] +pub(crate) enum DecryptionError { + CouldNotDecrypt, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_memory_encryption_key() { + let key = MemoryEncryptionKey::new(); + let data = b"Hello, world!"; + let encrypted = key.encrypt(data); + let decrypted = key.decrypt(&encrypted).unwrap(); + assert_eq!(data.as_ref(), decrypted.as_slice()); + } +} diff --git a/apps/desktop/desktop_native/core/src/secure_memory/secure_key/dpapi.rs b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/dpapi.rs new file mode 100644 index 00000000000..0975b542877 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/dpapi.rs @@ -0,0 +1,93 @@ +use super::crypto::{MemoryEncryptionKey, KEY_SIZE}; +use super::SecureKeyContainer; +use windows::Win32::Security::Cryptography::{ + CryptProtectMemory, CryptUnprotectMemory, CRYPTPROTECTMEMORY_BLOCK_SIZE, + CRYPTPROTECTMEMORY_SAME_PROCESS, +}; + +/// https://learn.microsoft.com/en-us/windows/win32/api/dpapi/nf-dpapi-cryptprotectdata +/// The DPAPI store encrypts data using the Windows Data Protection API (DPAPI). The key is bound +/// to the current process, and cannot be decrypted by other user-mode processes. +/// +/// Note: Admin processes can still decrypt this memory: +/// https://blog.slowerzs.net/posts/cryptdecryptmemory/ +pub(super) struct DpapiSecureKeyContainer { + dpapi_encrypted_key: [u8; KEY_SIZE + CRYPTPROTECTMEMORY_BLOCK_SIZE as usize], +} + +// SAFETY: The encrypted data is fully owned by this struct, and not exposed outside or cloned, +// and is disposed on drop of this struct. +unsafe impl Send for DpapiSecureKeyContainer {} +// SAFETY: The container is non-mutable and thus safe to share between threads. +unsafe impl Sync for DpapiSecureKeyContainer {} + +impl SecureKeyContainer for DpapiSecureKeyContainer { + fn as_key(&self) -> MemoryEncryptionKey { + let mut decrypted_key = self.dpapi_encrypted_key; + unsafe { + CryptUnprotectMemory( + decrypted_key.as_mut_ptr() as *mut core::ffi::c_void, + decrypted_key.len() as u32, + CRYPTPROTECTMEMORY_SAME_PROCESS, + ) + } + .expect("crypt_unprotect_memory should work"); + let mut key = [0u8; KEY_SIZE]; + key.copy_from_slice(&decrypted_key[..KEY_SIZE]); + MemoryEncryptionKey::from(&key) + } + + fn from_key(key: MemoryEncryptionKey) -> Self { + let mut padded_key = [0u8; KEY_SIZE + CRYPTPROTECTMEMORY_BLOCK_SIZE as usize]; + padded_key[..KEY_SIZE].copy_from_slice(key.as_ref()); + unsafe { + CryptProtectMemory( + padded_key.as_mut_ptr() as *mut core::ffi::c_void, + padded_key.len() as u32, + CRYPTPROTECTMEMORY_SAME_PROCESS, + ) + } + .expect("crypt_protect_memory should work"); + DpapiSecureKeyContainer { + dpapi_encrypted_key: padded_key, + } + } + + fn is_supported() -> bool { + // DPAPI is supported on all Windows versions that we support. + true + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_multiple_keys() { + let key1 = MemoryEncryptionKey::new(); + let key2 = MemoryEncryptionKey::new(); + let container1 = DpapiSecureKeyContainer::from_key(key1); + let container2 = DpapiSecureKeyContainer::from_key(key2); + + // Capture at time 1 + let data_1_1 = container1.as_key(); + let data_2_1 = container2.as_key(); + // Capture at time 2 + let data_1_2 = container1.as_key(); + let data_2_2 = container2.as_key(); + + // Same keys should be equal + assert_eq!(data_1_1.as_ref(), data_1_2.as_ref()); + assert_eq!(data_2_1.as_ref(), data_2_2.as_ref()); + + // Different keys should be different + assert_ne!(data_1_1.as_ref(), data_2_1.as_ref()); + assert_ne!(data_1_2.as_ref(), data_2_2.as_ref()); + } + + #[test] + fn test_is_supported() { + assert!(DpapiSecureKeyContainer::is_supported()); + } +} diff --git a/apps/desktop/desktop_native/core/src/secure_memory/secure_key/keyctl.rs b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/keyctl.rs new file mode 100644 index 00000000000..a738d964671 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/keyctl.rs @@ -0,0 +1,100 @@ +use crate::secure_memory::secure_key::crypto::MemoryEncryptionKey; + +use super::crypto::KEY_SIZE; +use super::SecureKeyContainer; +use linux_keyutils::{KeyRing, KeyRingIdentifier}; + +/// The keys are bound to the process keyring. +const KEY_RING_IDENTIFIER: KeyRingIdentifier = KeyRingIdentifier::Process; +/// This is an atomic global counter used to help generate unique key IDs +static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); +/// Generates a unique ID for the key in the kernel keyring. +/// SAFETY: This function is safe to call from multiple threads because it uses an atomic counter. +fn make_id() -> String { + let counter = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + // In case multiple processes are running, include the PID in the key ID. + let pid = std::process::id(); + format!("bitwarden_desktop_{}_{}", pid, counter) +} + +/// A secure key container that uses the Linux kernel keyctl API to store the key. +/// `https://man7.org/linux/man-pages/man1/keyctl.1.html`. The kernel enforces only +/// the correct process can read them, and they do not live in process memory space +/// and cannot be dumped. +pub(super) struct KeyctlSecureKeyContainer { + /// The kernel has an identifier for the key. This is randomly generated on construction. + id: String, +} + +// SAFETY: The key id is fully owned by this struct and not exposed or cloned, and cleaned up on drop. +// Further, since we use `KeyRingIdentifier::Process` and not `KeyRingIdentifier::Thread`, the key +// is accessible across threads within the same process bound. +unsafe impl Send for KeyctlSecureKeyContainer {} +// SAFETY: The container is non-mutable and thus safe to share between threads. +unsafe impl Sync for KeyctlSecureKeyContainer {} + +impl SecureKeyContainer for KeyctlSecureKeyContainer { + fn as_key(&self) -> MemoryEncryptionKey { + let ring = KeyRing::from_special_id(KEY_RING_IDENTIFIER, false) + .expect("should get process keyring"); + let key = ring.search(&self.id).expect("should find key"); + let mut buffer = [0u8; KEY_SIZE]; + key.read(&mut buffer).expect("should read key"); + MemoryEncryptionKey::from(&buffer) + } + + fn from_key(data: MemoryEncryptionKey) -> Self { + let ring = KeyRing::from_special_id(KEY_RING_IDENTIFIER, true) + .expect("should get process keyring"); + let id = make_id(); + ring.add_key(&id, &data).expect("should add key"); + KeyctlSecureKeyContainer { id } + } + + fn is_supported() -> bool { + KeyRing::from_special_id(KEY_RING_IDENTIFIER, true).is_ok() + } +} + +impl Drop for KeyctlSecureKeyContainer { + fn drop(&mut self) { + let ring = KeyRing::from_special_id(KEY_RING_IDENTIFIER, false) + .expect("should get process keyring"); + if let Ok(key) = ring.search(&self.id) { + let _ = key.invalidate(); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_multiple_keys() { + let key1 = MemoryEncryptionKey::new(); + let key2 = MemoryEncryptionKey::new(); + let container1 = KeyctlSecureKeyContainer::from_key(key1); + let container2 = KeyctlSecureKeyContainer::from_key(key2); + + // Capture at time 1 + let data_1_1 = container1.as_key(); + let data_2_1 = container2.as_key(); + // Capture at time 2 + let data_1_2 = container1.as_key(); + let data_2_2 = container2.as_key(); + + // Same keys should be equal + assert_eq!(data_1_1.as_ref(), data_1_2.as_ref()); + assert_eq!(data_2_1.as_ref(), data_2_2.as_ref()); + + // Different keys should be different + assert_ne!(data_1_1.as_ref(), data_2_1.as_ref()); + assert_ne!(data_1_2.as_ref(), data_2_2.as_ref()); + } + + #[test] + fn test_is_supported() { + assert!(KeyctlSecureKeyContainer::is_supported()); + } +} diff --git a/apps/desktop/desktop_native/core/src/secure_memory/secure_key/memfd_secret.rs b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/memfd_secret.rs new file mode 100644 index 00000000000..4e6a2c4d7ac --- /dev/null +++ b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/memfd_secret.rs @@ -0,0 +1,109 @@ +use std::{ptr::NonNull, sync::LazyLock}; + +use super::crypto::MemoryEncryptionKey; +use super::crypto::KEY_SIZE; +use super::SecureKeyContainer; + +/// https://man.archlinux.org/man/memfd_secret.2.en +/// The memfd_secret store protects the data using the `memfd_secret` syscall. The +/// data is inaccessible to other user-mode processes, and even to root in most cases. +/// If arbitrary data can be executed in the kernel, the data can still be retrieved: +/// https://github.com/JonathonReinhart/nosecmem +pub(super) struct MemfdSecretSecureKeyContainer { + ptr: NonNull<[u8]>, +} +// SAFETY: The pointers in this struct are allocated by `memfd_secret`, and we have full ownership. +// They are never exposed outside or cloned, and are cleaned up by drop. +unsafe impl Send for MemfdSecretSecureKeyContainer {} +// SAFETY: The container is non-mutable and thus safe to share between threads. Further, memfd-secret +// is accessible across threads within the same process bound. +unsafe impl Sync for MemfdSecretSecureKeyContainer {} + +impl SecureKeyContainer for MemfdSecretSecureKeyContainer { + fn as_key(&self) -> MemoryEncryptionKey { + MemoryEncryptionKey::from( + &unsafe { self.ptr.as_ref() } + .try_into() + .expect("slice should be KEY_SIZE"), + ) + } + + fn from_key(key: MemoryEncryptionKey) -> Self { + let mut ptr: NonNull<[u8]> = unsafe { + memsec::memfd_secret_sized(KEY_SIZE).expect("memfd_secret_sized should work") + }; + unsafe { + std::ptr::copy_nonoverlapping( + key.as_ref().as_ptr(), + ptr.as_mut().as_mut_ptr(), + KEY_SIZE, + ); + } + MemfdSecretSecureKeyContainer { ptr } + } + + /// Note, `memfd_secret` is only available since Linux 6.5, so fallbacks are needed. + fn is_supported() -> bool { + // To test if memfd_secret is supported, we try to allocate a 1 byte and see if that + // succeeds. + static IS_SUPPORTED: LazyLock = LazyLock::new(|| { + let Some(ptr): Option> = (unsafe { memsec::memfd_secret_sized(1) }) + else { + return false; + }; + + // Check that the pointer is readable and writable + let result = unsafe { + let ptr = ptr.as_ptr() as *mut u8; + *ptr = 30; + *ptr += 107; + *ptr == 137 + }; + + unsafe { memsec::free_memfd_secret(ptr) }; + result + }); + *IS_SUPPORTED + } +} + +impl Drop for MemfdSecretSecureKeyContainer { + fn drop(&mut self) { + unsafe { + memsec::free_memfd_secret(self.ptr); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_multiple_keys() { + let key1 = MemoryEncryptionKey::new(); + let key2 = MemoryEncryptionKey::new(); + let container1 = MemfdSecretSecureKeyContainer::from_key(key1); + let container2 = MemfdSecretSecureKeyContainer::from_key(key2); + + // Capture at time 1 + let data_1_1 = container1.as_key(); + let data_2_1 = container2.as_key(); + // Capture at time 2 + let data_1_2 = container1.as_key(); + let data_2_2 = container2.as_key(); + + // Same keys should be equal + assert_eq!(data_1_1.as_ref(), data_1_2.as_ref()); + assert_eq!(data_2_1.as_ref(), data_2_2.as_ref()); + + // Different keys should be different + assert_ne!(data_1_1.as_ref(), data_2_1.as_ref()); + assert_ne!(data_1_2.as_ref(), data_2_2.as_ref()); + } + + #[test] + fn test_is_supported() { + assert!(MemfdSecretSecureKeyContainer::is_supported()); + } +} diff --git a/apps/desktop/desktop_native/core/src/secure_memory/secure_key/mlock.rs b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/mlock.rs new file mode 100644 index 00000000000..db21cd7fedc --- /dev/null +++ b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/mlock.rs @@ -0,0 +1,83 @@ +use std::ptr::NonNull; + +use super::crypto::MemoryEncryptionKey; +use super::crypto::KEY_SIZE; +use super::SecureKeyContainer; + +/// A SecureKeyContainer that uses mlock to prevent the memory from being swapped to disk. +/// This does not provide as strong protections as other methods, but is always supported. +pub(super) struct MlockSecureKeyContainer { + ptr: NonNull<[u8]>, +} +// SAFETY: The pointers in this struct are allocated by `malloc_sized`, and we have full ownership. +// They are never exposed outside or cloned, and are cleaned up by drop. +unsafe impl Send for MlockSecureKeyContainer {} +// SAFETY: The container is non-mutable and thus safe to share between threads. +unsafe impl Sync for MlockSecureKeyContainer {} + +impl SecureKeyContainer for MlockSecureKeyContainer { + fn as_key(&self) -> MemoryEncryptionKey { + MemoryEncryptionKey::from( + &unsafe { self.ptr.as_ref() } + .try_into() + .expect("slice should be KEY_SIZE"), + ) + } + fn from_key(key: MemoryEncryptionKey) -> Self { + let mut ptr: NonNull<[u8]> = + unsafe { memsec::malloc_sized(KEY_SIZE).expect("malloc_sized should work") }; + unsafe { + std::ptr::copy_nonoverlapping( + key.as_ref().as_ptr(), + ptr.as_mut().as_mut_ptr(), + KEY_SIZE, + ); + } + MlockSecureKeyContainer { ptr } + } + + fn is_supported() -> bool { + true + } +} + +impl Drop for MlockSecureKeyContainer { + fn drop(&mut self) { + unsafe { + memsec::free(self.ptr); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_multiple_keys() { + let key1 = MemoryEncryptionKey::new(); + let key2 = MemoryEncryptionKey::new(); + let container1 = MlockSecureKeyContainer::from_key(key1); + let container2 = MlockSecureKeyContainer::from_key(key2); + + // Capture at time 1 + let data_1_1 = container1.as_key(); + let data_2_1 = container2.as_key(); + // Capture at time 2 + let data_1_2 = container1.as_key(); + let data_2_2 = container2.as_key(); + + // Same keys should be equal + assert_eq!(data_1_1.as_ref(), data_1_2.as_ref()); + assert_eq!(data_2_1.as_ref(), data_2_2.as_ref()); + + // Different keys should be different + assert_ne!(data_1_1.as_ref(), data_2_1.as_ref()); + assert_ne!(data_1_2.as_ref(), data_2_2.as_ref()); + } + + #[test] + fn test_is_supported() { + assert!(MlockSecureKeyContainer::is_supported()); + } +} diff --git a/apps/desktop/desktop_native/core/src/secure_memory/secure_key/mod.rs b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/mod.rs new file mode 100644 index 00000000000..6c3b53117a5 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/secure_memory/secure_key/mod.rs @@ -0,0 +1,242 @@ +//! This module provides hardened storage for single cryptographic keys. These are meant for encrypting large amounts of memory. +//! Some platforms restrict how many keys can be protected by their APIs, which necessitates this layer of indirection. This significantly +//! reduces the complexity of each platform specific implementation, since all that's needed is implementing protecting a single fixed sized key +//! instead of protecting many arbitrarily sized secrets. This significantly lowers the effort to maintain each implementation. +//! +//! The implementations include DPAPI on Windows, `keyctl` on Linux, and `memfd_secret` on Linux, and a fallback implementation using mlock. + +use tracing::info; + +mod crypto; +#[cfg(target_os = "windows")] +mod dpapi; +#[cfg(target_os = "linux")] +mod keyctl; +#[cfg(target_os = "linux")] +mod memfd_secret; +mod mlock; + +pub use crypto::EncryptedMemory; + +use crate::secure_memory::secure_key::crypto::DecryptionError; + +/// An ephemeral key that is protected using a platform mechanism. It is generated on construction freshly, and can be used +/// to encrypt and decrypt segments of memory. Since the key is ephemeral, persistent data cannot be encrypted with this key. +/// On Linux and Windows, in most cases the protection mechanisms prevent memory dumps/debuggers from reading the key. +/// +/// Note: This can be circumvented if code can be injected into the process and is only effective in combination with the +/// memory isolation provided in `process_isolation`. +/// - https://github.com/zer1t0/keydump +#[allow(unused)] +pub(crate) struct SecureMemoryEncryptionKey(CrossPlatformSecureKeyContainer); + +impl SecureMemoryEncryptionKey { + pub fn new() -> Self { + SecureMemoryEncryptionKey(CrossPlatformSecureKeyContainer::from_key( + crypto::MemoryEncryptionKey::new(), + )) + } + + /// Encrypts the provided plaintext using the contained key, returning an EncryptedMemory blob. + #[allow(unused)] + pub fn encrypt(&self, plaintext: &[u8]) -> crypto::EncryptedMemory { + self.0.as_key().encrypt(plaintext) + } + + /// Decrypts the provided EncryptedMemory blob using the contained key, returning the plaintext. + /// If the decryption fails, that means the memory was tampered with, and the function panics. + #[allow(unused)] + pub fn decrypt(&self, encrypted: &crypto::EncryptedMemory) -> Result, DecryptionError> { + self.0.as_key().decrypt(encrypted) + } +} + +/// A platform specific implementation of a key container that protects a single encryption key +/// from memory attacks. +#[allow(unused)] +trait SecureKeyContainer: Sync + Send { + /// Returns the key as a byte slice. This slice does not have additional memory protections applied. + fn as_key(&self) -> crypto::MemoryEncryptionKey; + /// Creates a new SecureKeyContainer from the provided key. + fn from_key(key: crypto::MemoryEncryptionKey) -> Self; + /// Returns true if this platform supports this secure key container implementation. + fn is_supported() -> bool; +} + +#[allow(unused)] +enum CrossPlatformSecureKeyContainer { + #[cfg(target_os = "windows")] + Dpapi(dpapi::DpapiSecureKeyContainer), + #[cfg(target_os = "linux")] + Keyctl(keyctl::KeyctlSecureKeyContainer), + #[cfg(target_os = "linux")] + MemfdSecret(memfd_secret::MemfdSecretSecureKeyContainer), + Mlock(mlock::MlockSecureKeyContainer), +} + +impl SecureKeyContainer for CrossPlatformSecureKeyContainer { + fn as_key(&self) -> crypto::MemoryEncryptionKey { + match self { + #[cfg(target_os = "windows")] + CrossPlatformSecureKeyContainer::Dpapi(c) => c.as_key(), + #[cfg(target_os = "linux")] + CrossPlatformSecureKeyContainer::Keyctl(c) => c.as_key(), + #[cfg(target_os = "linux")] + CrossPlatformSecureKeyContainer::MemfdSecret(c) => c.as_key(), + CrossPlatformSecureKeyContainer::Mlock(c) => c.as_key(), + } + } + + fn from_key(key: crypto::MemoryEncryptionKey) -> Self { + if let Some(container) = get_env_forced_container() { + return container; + } + + #[cfg(target_os = "windows")] + { + if dpapi::DpapiSecureKeyContainer::is_supported() { + info!("Using DPAPI for secure key storage"); + return CrossPlatformSecureKeyContainer::Dpapi( + dpapi::DpapiSecureKeyContainer::from_key(key), + ); + } + } + #[cfg(target_os = "linux")] + { + // Memfd_secret is slightly better in some cases of the kernel being compromised. + // Note that keyctl may sometimes not be available in e.g. snap. Memfd_secret is + // not available on kernels older than 6.5 while keyctl is supported since 2.6. + // + // Note: This may prevent the system from hibernating but not sleeping. Hibernate + // would write the memory to disk, exposing the keys. If this is an issue, + // the environment variable `SECURE_KEY_CONTAINER_BACKEND` can be used + // to force the use of keyctl or mlock. + if memfd_secret::MemfdSecretSecureKeyContainer::is_supported() { + info!("Using memfd_secret for secure key storage"); + return CrossPlatformSecureKeyContainer::MemfdSecret( + memfd_secret::MemfdSecretSecureKeyContainer::from_key(key), + ); + } + if keyctl::KeyctlSecureKeyContainer::is_supported() { + info!("Using keyctl for secure key storage"); + return CrossPlatformSecureKeyContainer::Keyctl( + keyctl::KeyctlSecureKeyContainer::from_key(key), + ); + } + } + + // Falling back to mlock means that the key is accessible via memory dumping. + info!("Falling back to mlock for secure key storage"); + CrossPlatformSecureKeyContainer::Mlock(mlock::MlockSecureKeyContainer::from_key(key)) + } + + fn is_supported() -> bool { + // Mlock is always supported as a fallback. + true + } +} + +fn get_env_forced_container() -> Option { + let env_var = std::env::var("SECURE_KEY_CONTAINER_BACKEND"); + match env_var.as_deref() { + #[cfg(target_os = "windows")] + Ok("dpapi") => { + info!("Forcing DPAPI secure key container via environment variable"); + Some(CrossPlatformSecureKeyContainer::Dpapi( + dpapi::DpapiSecureKeyContainer::from_key(crypto::MemoryEncryptionKey::new()), + )) + } + #[cfg(target_os = "linux")] + Ok("memfd_secret") => { + info!("Forcing memfd_secret secure key container via environment variable"); + Some(CrossPlatformSecureKeyContainer::MemfdSecret( + memfd_secret::MemfdSecretSecureKeyContainer::from_key( + crypto::MemoryEncryptionKey::new(), + ), + )) + } + #[cfg(target_os = "linux")] + Ok("keyctl") => { + info!("Forcing keyctl secure key container via environment variable"); + Some(CrossPlatformSecureKeyContainer::Keyctl( + keyctl::KeyctlSecureKeyContainer::from_key(crypto::MemoryEncryptionKey::new()), + )) + } + Ok("mlock") => { + info!("Forcing mlock secure key container via environment variable"); + Some(CrossPlatformSecureKeyContainer::Mlock( + mlock::MlockSecureKeyContainer::from_key(crypto::MemoryEncryptionKey::new()), + )) + } + _ => { + info!( + "{} is not a valid secure key container backend, using automatic selection", + env_var.unwrap_or_default() + ); + None + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_multiple_keys() { + // Create 20 different keys + let original_keys: Vec = (0..20) + .map(|_| crypto::MemoryEncryptionKey::new()) + .collect(); + + // Store them in secure containers + let containers: Vec = original_keys + .iter() + .map(|key| { + let key_bytes: &[u8; crypto::KEY_SIZE] = key.as_ref().try_into().unwrap(); + CrossPlatformSecureKeyContainer::from_key(crypto::MemoryEncryptionKey::from( + key_bytes, + )) + }) + .collect(); + + // Read all keys back and validate they match the originals + for (i, (original_key, container)) in + original_keys.iter().zip(containers.iter()).enumerate() + { + let retrieved_key = container.as_key(); + assert_eq!( + original_key.as_ref(), + retrieved_key.as_ref(), + "Key {} should match after storage and retrieval", + i + ); + } + + // Verify all keys are different from each other + for i in 0..original_keys.len() { + for j in (i + 1)..original_keys.len() { + assert_ne!( + original_keys[i].as_ref(), + original_keys[j].as_ref(), + "Keys {} and {} should be different", + i, + j + ); + } + } + + // Read keys back a second time to ensure consistency + for (i, (original_key, container)) in + original_keys.iter().zip(containers.iter()).enumerate() + { + let retrieved_key_again = container.as_key(); + assert_eq!( + original_key.as_ref(), + retrieved_key_again.as_ref(), + "Key {} should still match on second retrieval", + i + ); + } + } +} From 7321e3132bb19611d6453b0744165f09d7d62fdc Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 23 Oct 2025 09:13:26 -0500 Subject: [PATCH 07/73] [PM-26793] Fetch premium plan from pricing service (#16858) * Fetch premium plan from pricing service * Resolve Claude feedback --- .../at-risk-passwords.component.ts | 2 +- .../individual/premium/premium.component.html | 279 ++++++++++-------- .../individual/premium/premium.component.ts | 134 +++++---- .../subscription-pricing.service.spec.ts | 249 +++++++++++++++- .../services/subscription-pricing.service.ts | 89 ++++-- .../billing-api.service.abstraction.ts | 4 + .../models/response/premium-plan.response.ts | 47 +++ .../billing/services/billing-api.service.ts | 9 +- libs/common/src/enums/feature-flag.enum.ts | 2 + 9 files changed, 591 insertions(+), 224 deletions(-) create mode 100644 libs/common/src/billing/models/response/premium-plan.response.ts diff --git a/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.ts b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.ts index 6551c84a4e2..6918bedb9bf 100644 --- a/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.ts +++ b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.ts @@ -104,7 +104,7 @@ export class AtRiskPasswordsComponent implements OnInit { * The UI utilize a bitBadge which does not support async actions (like bitButton does). * @protected */ - protected launchingCipher = signal(null); + protected readonly launchingCipher = signal(null); private activeUserData$ = this.accountService.activeAccount$.pipe( filterOutNullish(), diff --git a/apps/web/src/app/billing/individual/premium/premium.component.html b/apps/web/src/app/billing/individual/premium/premium.component.html index d08b942ff8b..39b32be0853 100644 --- a/apps/web/src/app/billing/individual/premium/premium.component.html +++ b/apps/web/src/app/billing/individual/premium/premium.component.html @@ -1,132 +1,153 @@ - - -

{{ "goPremium" | i18n }}

- - {{ "alreadyPremiumFromOrg" | i18n }} - - -

{{ "premiumUpgradeUnlockFeatures" | i18n }}

-
    -
  • - - {{ "premiumSignUpStorage" | i18n }} -
  • -
  • - - {{ "premiumSignUpTwoStepOptions" | i18n }} -
  • -
  • - - {{ "premiumSignUpEmergency" | i18n }} -
  • -
  • - - {{ "premiumSignUpReports" | i18n }} -
  • -
  • - - {{ "premiumSignUpTotp" | i18n }} -
  • -
  • - - {{ "premiumSignUpSupport" | i18n }} -
  • -
  • - - {{ "premiumSignUpFuture" | i18n }} -
  • -
-

- {{ - "premiumPriceWithFamilyPlan" - | i18n: (premiumPrice | currency: "$") : familyPlanMaxUserCount - }} - - {{ "bitwardenFamiliesPlan" | i18n }} - -

- + + {{ "loading" | i18n }} + +} @else { + + +

{{ "goPremium" | i18n }}

+ - {{ "purchasePremium" | i18n }} -
-
-
- - - -
- -

{{ "addons" | i18n }}

-
- - {{ "additionalStorageGb" | i18n }} - - {{ - "additionalStorageIntervalDesc" - | i18n: "1 GB" : (storageGBPrice | currency: "$") : ("year" | i18n) - }} - -
-
- -

{{ "summary" | i18n }}

- {{ "premiumMembership" | i18n }}: {{ premiumPrice | currency: "$" }}
- {{ "additionalStorageGb" | i18n }}: {{ formGroup.value.additionalStorage || 0 }} GB × - {{ storageGBPrice | currency: "$" }} = - {{ additionalStorageCost | currency: "$" }} -
-
- -

{{ "paymentInformation" | i18n }}

-
- + +

{{ "premiumUpgradeUnlockFeatures" | i18n }}

+
    +
  • + + {{ "premiumSignUpStorage" | i18n }} +
  • +
  • + + {{ "premiumSignUpTwoStepOptions" | i18n }} +
  • +
  • + + {{ "premiumSignUpEmergency" | i18n }} +
  • +
  • + + {{ "premiumSignUpReports" | i18n }} +
  • +
  • + + {{ "premiumSignUpTotp" | i18n }} +
  • +
  • + + {{ "premiumSignUpSupport" | i18n }} +
  • +
  • + + {{ "premiumSignUpFuture" | i18n }} +
  • +
+

+ {{ + "premiumPriceWithFamilyPlan" + | i18n: (premiumPrice$ | async | currency: "$") : familyPlanMaxUserCount + }} + + {{ "bitwardenFamiliesPlan" | i18n }} + +

+ -
- - -
-
-
- {{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }} - {{ "estimatedTax" | i18n }}: {{ estimatedTax | currency: "USD $" }} + {{ "purchasePremium" | i18n }} + + + + + + + + +

{{ "addons" | i18n }}

+
+ + {{ "additionalStorageGb" | i18n }} + + {{ + "additionalStorageIntervalDesc" + | i18n: "1 GB" : (storagePrice$ | async | currency: "$") : ("year" | i18n) + }} +
-
-
-

- {{ "total" | i18n }}: {{ total | currency: "USD $" }}/{{ "year" | i18n }} -

- - - - + + +

{{ "summary" | i18n }}

+ {{ "premiumMembership" | i18n }}: {{ premiumPrice$ | async | currency: "$" }}
+ {{ "additionalStorageGb" | i18n }}: {{ formGroup.value.additionalStorage || 0 }} GB × + {{ storagePrice$ | async | currency: "$" }} = + {{ storageCost$ | async | currency: "$" }} +
+
+ +

{{ "paymentInformation" | i18n }}

+
+ + + + +
+
+
+ {{ "planPrice" | i18n }}: {{ subtotal$ | async | currency: "USD $" }} + {{ "estimatedTax" | i18n }}: {{ tax$ | async | currency: "USD $" }} +
+
+
+

+ {{ "total" | i18n }}: {{ total$ | async | currency: "USD $" }}/{{ + "year" | i18n + }} +

+ +
+ + +} diff --git a/apps/web/src/app/billing/individual/premium/premium.component.ts b/apps/web/src/app/billing/individual/premium/premium.component.ts index d541ab95b95..526b020a9e3 100644 --- a/apps/web/src/app/billing/individual/premium/premium.component.ts +++ b/apps/web/src/app/billing/individual/premium/premium.component.ts @@ -4,7 +4,19 @@ import { Component, ViewChild } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormControl, FormGroup, Validators } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; -import { combineLatest, concatMap, from, map, Observable, of, startWith, switchMap } from "rxjs"; +import { + combineLatest, + concatMap, + filter, + from, + map, + Observable, + of, + startWith, + switchMap, + catchError, + shareReplay, +} from "rxjs"; import { debounceTime } from "rxjs/operators"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -26,7 +38,9 @@ import { tokenizablePaymentMethodToLegacyEnum, NonTokenizablePaymentMethods, } from "@bitwarden/web-vault/app/billing/payment/types"; +import { SubscriptionPricingService } from "@bitwarden/web-vault/app/billing/services/subscription-pricing.service"; import { mapAccountToSubscriber } from "@bitwarden/web-vault/app/billing/types"; +import { PersonalSubscriptionPricingTierIds } from "@bitwarden/web-vault/app/billing/types/subscription-pricing-tier"; @Component({ templateUrl: "./premium.component.html", @@ -37,7 +51,6 @@ export class PremiumComponent { @ViewChild(EnterPaymentMethodComponent) enterPaymentMethodComponent!: EnterPaymentMethodComponent; protected hasPremiumFromAnyOrganization$: Observable; - protected accountCredit$: Observable; protected hasEnoughAccountCredit$: Observable; protected formGroup = new FormGroup({ @@ -46,13 +59,66 @@ export class PremiumComponent { billingAddress: EnterBillingAddressComponent.getFormGroup(), }); + premiumPrices$ = this.subscriptionPricingService.getPersonalSubscriptionPricingTiers$().pipe( + map((tiers) => { + const premiumPlan = tiers.find( + (tier) => tier.id === PersonalSubscriptionPricingTierIds.Premium, + ); + + if (!premiumPlan) { + throw new Error("Could not find Premium plan"); + } + + return { + seat: premiumPlan.passwordManager.annualPrice, + storage: premiumPlan.passwordManager.annualPricePerAdditionalStorageGB, + }; + }), + shareReplay({ bufferSize: 1, refCount: true }), + ); + + premiumPrice$ = this.premiumPrices$.pipe(map((prices) => prices.seat)); + + storagePrice$ = this.premiumPrices$.pipe(map((prices) => prices.storage)); + + protected isLoadingPrices$ = this.premiumPrices$.pipe( + map(() => false), + startWith(true), + catchError(() => of(false)), + ); + + storageCost$ = combineLatest([ + this.storagePrice$, + this.formGroup.controls.additionalStorage.valueChanges.pipe( + startWith(this.formGroup.value.additionalStorage), + ), + ]).pipe(map(([storagePrice, additionalStorage]) => storagePrice * additionalStorage)); + + subtotal$ = combineLatest([this.premiumPrice$, this.storageCost$]).pipe( + map(([premiumPrice, storageCost]) => premiumPrice + storageCost), + ); + + tax$ = this.formGroup.valueChanges.pipe( + filter(() => this.formGroup.valid), + debounceTime(1000), + switchMap(async () => { + const billingAddress = getBillingAddressFromForm(this.formGroup.controls.billingAddress); + const taxAmounts = await this.taxClient.previewTaxForPremiumSubscriptionPurchase( + this.formGroup.value.additionalStorage, + billingAddress, + ); + return taxAmounts.tax; + }), + startWith(0), + ); + + total$ = combineLatest([this.subtotal$, this.tax$]).pipe( + map(([subtotal, tax]) => subtotal + tax), + ); + protected cloudWebVaultURL: string; protected isSelfHost = false; - - protected estimatedTax: number = 0; protected readonly familyPlanMaxUserCount = 6; - protected readonly premiumPrice = 10; - protected readonly storageGBPrice = 4; constructor( private activatedRoute: ActivatedRoute, @@ -67,6 +133,7 @@ export class PremiumComponent { private accountService: AccountService, private subscriberBillingClient: SubscriberBillingClient, private taxClient: TaxClient, + private subscriptionPricingService: SubscriptionPricingService, ) { this.isSelfHost = this.platformUtilsService.isSelfHost(); @@ -76,23 +143,23 @@ export class PremiumComponent { ), ); - // Fetch account credit - this.accountCredit$ = this.accountService.activeAccount$.pipe( + const accountCredit$ = this.accountService.activeAccount$.pipe( mapAccountToSubscriber, switchMap((account) => this.subscriberBillingClient.getCredit(account)), ); - // Check if user has enough account credit for the purchase this.hasEnoughAccountCredit$ = combineLatest([ - this.accountCredit$, - this.formGroup.valueChanges.pipe(startWith(this.formGroup.value)), + accountCredit$, + this.total$, + this.formGroup.controls.paymentMethod.controls.type.valueChanges.pipe( + startWith(this.formGroup.value.paymentMethod.type), + ), ]).pipe( - map(([credit, formValue]) => { - const selectedPaymentType = formValue.paymentMethod?.type; - if (selectedPaymentType !== NonTokenizablePaymentMethods.accountCredit) { - return true; // Not using account credit, so this check doesn't apply + map(([credit, total, paymentMethod]) => { + if (paymentMethod !== NonTokenizablePaymentMethods.accountCredit) { + return true; } - return credit >= this.total; + return credit >= total; }), ); @@ -116,14 +183,6 @@ export class PremiumComponent { }), ) .subscribe(); - - this.formGroup.valueChanges - .pipe( - debounceTime(1000), - switchMap(async () => await this.refreshSalesTax()), - takeUntilDestroyed(), - ) - .subscribe(); } finalizeUpgrade = async () => { @@ -177,38 +236,11 @@ export class PremiumComponent { await this.postFinalizeUpgrade(); }; - protected get additionalStorageCost(): number { - return this.storageGBPrice * this.formGroup.value.additionalStorage; - } - protected get premiumURL(): string { return `${this.cloudWebVaultURL}/#/settings/subscription/premium`; } - protected get subtotal(): number { - return this.premiumPrice + this.additionalStorageCost; - } - - protected get total(): number { - return this.subtotal + this.estimatedTax; - } - protected async onLicenseFileSelectedChanged(): Promise { await this.postFinalizeUpgrade(); } - - private async refreshSalesTax(): Promise { - if (this.formGroup.invalid) { - return; - } - - const billingAddress = getBillingAddressFromForm(this.formGroup.controls.billingAddress); - - const taxAmounts = await this.taxClient.previewTaxForPremiumSubscriptionPurchase( - this.formGroup.value.additionalStorage, - billingAddress, - ); - - this.estimatedTax = taxAmounts.tax; - } } diff --git a/apps/web/src/app/billing/services/subscription-pricing.service.spec.ts b/apps/web/src/app/billing/services/subscription-pricing.service.spec.ts index 0fb33020bc3..de80cdcbdbf 100644 --- a/apps/web/src/app/billing/services/subscription-pricing.service.spec.ts +++ b/apps/web/src/app/billing/services/subscription-pricing.service.spec.ts @@ -1,9 +1,12 @@ import { TestBed } from "@angular/core/testing"; import { mock, MockProxy } from "jest-mock-extended"; +import { of } from "rxjs"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; +import { PremiumPlanResponse } from "@bitwarden/common/billing/models/response/premium-plan.response"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ToastService } from "@bitwarden/components"; import { LogService } from "@bitwarden/logging"; @@ -18,7 +21,8 @@ import { SubscriptionPricingService } from "./subscription-pricing.service"; describe("SubscriptionPricingService", () => { let service: SubscriptionPricingService; - let apiService: MockProxy; + let billingApiService: MockProxy; + let configService: MockProxy; let i18nService: MockProxy; let logService: MockProxy; let toastService: MockProxy; @@ -217,6 +221,15 @@ describe("SubscriptionPricingService", () => { continuationToken: null, }; + const mockPremiumPlanResponse: PremiumPlanResponse = { + seat: { + price: 10, + }, + storage: { + price: 4, + }, + } as PremiumPlanResponse; + beforeAll(() => { i18nService = mock(); logService = mock(); @@ -320,14 +333,18 @@ describe("SubscriptionPricingService", () => { }); beforeEach(() => { - apiService = mock(); + billingApiService = mock(); + configService = mock(); - apiService.getPlans.mockResolvedValue(mockPlansResponse); + billingApiService.getPlans.mockResolvedValue(mockPlansResponse); + billingApiService.getPremiumPlan.mockResolvedValue(mockPremiumPlanResponse); + configService.getFeatureFlag$.mockReturnValue(of(false)); // Default to false (use hardcoded value) TestBed.configureTestingModule({ providers: [ SubscriptionPricingService, - { provide: ApiService, useValue: apiService }, + { provide: BillingApiServiceAbstraction, useValue: billingApiService }, + { provide: ConfigService, useValue: configService }, { provide: I18nService, useValue: i18nService }, { provide: LogService, useValue: logService }, { provide: ToastService, useValue: toastService }, @@ -406,13 +423,16 @@ describe("SubscriptionPricingService", () => { }); it("should handle API errors by logging and showing toast", (done) => { - const errorApiService = mock(); + const errorBillingApiService = mock(); + const errorConfigService = mock(); const errorI18nService = mock(); const errorLogService = mock(); const errorToastService = mock(); const testError = new Error("API error"); - errorApiService.getPlans.mockRejectedValue(testError); + errorBillingApiService.getPlans.mockRejectedValue(testError); + errorBillingApiService.getPremiumPlan.mockResolvedValue(mockPremiumPlanResponse); + errorConfigService.getFeatureFlag$.mockReturnValue(of(false)); errorI18nService.t.mockImplementation((key: string) => { if (key === "unexpectedError") { @@ -422,7 +442,8 @@ describe("SubscriptionPricingService", () => { }); const errorService = new SubscriptionPricingService( - errorApiService, + errorBillingApiService, + errorConfigService, errorI18nService, errorLogService, errorToastService, @@ -591,13 +612,16 @@ describe("SubscriptionPricingService", () => { }); it("should handle API errors by logging and showing toast", (done) => { - const errorApiService = mock(); + const errorBillingApiService = mock(); + const errorConfigService = mock(); const errorI18nService = mock(); const errorLogService = mock(); const errorToastService = mock(); const testError = new Error("API error"); - errorApiService.getPlans.mockRejectedValue(testError); + errorBillingApiService.getPlans.mockRejectedValue(testError); + errorBillingApiService.getPremiumPlan.mockResolvedValue(mockPremiumPlanResponse); + errorConfigService.getFeatureFlag$.mockReturnValue(of(false)); errorI18nService.t.mockImplementation((key: string) => { if (key === "unexpectedError") { @@ -607,7 +631,8 @@ describe("SubscriptionPricingService", () => { }); const errorService = new SubscriptionPricingService( - errorApiService, + errorBillingApiService, + errorConfigService, errorI18nService, errorLogService, errorToastService, @@ -831,13 +856,16 @@ describe("SubscriptionPricingService", () => { }); it("should handle API errors by logging and showing toast", (done) => { - const errorApiService = mock(); + const errorBillingApiService = mock(); + const errorConfigService = mock(); const errorI18nService = mock(); const errorLogService = mock(); const errorToastService = mock(); const testError = new Error("API error"); - errorApiService.getPlans.mockRejectedValue(testError); + errorBillingApiService.getPlans.mockRejectedValue(testError); + errorBillingApiService.getPremiumPlan.mockResolvedValue(mockPremiumPlanResponse); + errorConfigService.getFeatureFlag$.mockReturnValue(of(false)); errorI18nService.t.mockImplementation((key: string) => { if (key === "unexpectedError") { @@ -847,7 +875,8 @@ describe("SubscriptionPricingService", () => { }); const errorService = new SubscriptionPricingService( - errorApiService, + errorBillingApiService, + errorConfigService, errorI18nService, errorLogService, errorToastService, @@ -871,9 +900,137 @@ describe("SubscriptionPricingService", () => { }); }); + describe("Edge case handling", () => { + it("should handle getPremiumPlan() error when getPlans() succeeds", (done) => { + const errorBillingApiService = mock(); + const errorConfigService = mock(); + + const testError = new Error("Premium plan API error"); + errorBillingApiService.getPlans.mockResolvedValue(mockPlansResponse); + errorBillingApiService.getPremiumPlan.mockRejectedValue(testError); + errorConfigService.getFeatureFlag$.mockReturnValue(of(true)); // Enable feature flag to use premium plan API + + const errorService = new SubscriptionPricingService( + errorBillingApiService, + errorConfigService, + i18nService, + logService, + toastService, + ); + + errorService.getPersonalSubscriptionPricingTiers$().subscribe({ + next: (tiers) => { + // Should return empty array due to error in premium plan fetch + expect(tiers).toEqual([]); + expect(logService.error).toHaveBeenCalledWith( + "Failed to fetch premium plan from API", + testError, + ); + expect(toastService.showToast).toHaveBeenCalledWith({ + variant: "error", + title: "", + message: "An unexpected error has occurred.", + }); + done(); + }, + error: () => { + fail("Observable should not error, it should return empty array"); + }, + }); + }); + + it("should handle malformed premium plan API response", (done) => { + const errorBillingApiService = mock(); + const errorConfigService = mock(); + + // Malformed response missing the Seat property + const malformedResponse = { + Storage: { + StripePriceId: "price_storage", + Price: 4, + }, + }; + + errorBillingApiService.getPlans.mockResolvedValue(mockPlansResponse); + errorBillingApiService.getPremiumPlan.mockResolvedValue(malformedResponse as any); + errorConfigService.getFeatureFlag$.mockReturnValue(of(true)); // Enable feature flag + + const errorService = new SubscriptionPricingService( + errorBillingApiService, + errorConfigService, + i18nService, + logService, + toastService, + ); + + errorService.getPersonalSubscriptionPricingTiers$().subscribe({ + next: (tiers) => { + // Should return empty array due to validation error + expect(tiers).toEqual([]); + expect(logService.error).toHaveBeenCalled(); + expect(toastService.showToast).toHaveBeenCalledWith({ + variant: "error", + title: "", + message: "An unexpected error has occurred.", + }); + done(); + }, + error: () => { + fail("Observable should not error, it should return empty array"); + }, + }); + }); + + it("should handle malformed premium plan with invalid price types", (done) => { + const errorBillingApiService = mock(); + const errorConfigService = mock(); + + // Malformed response with price as string instead of number + const malformedResponse = { + Seat: { + StripePriceId: "price_seat", + Price: "10", // Should be a number + }, + Storage: { + StripePriceId: "price_storage", + Price: 4, + }, + }; + + errorBillingApiService.getPlans.mockResolvedValue(mockPlansResponse); + errorBillingApiService.getPremiumPlan.mockResolvedValue(malformedResponse as any); + errorConfigService.getFeatureFlag$.mockReturnValue(of(true)); // Enable feature flag + + const errorService = new SubscriptionPricingService( + errorBillingApiService, + errorConfigService, + i18nService, + logService, + toastService, + ); + + errorService.getPersonalSubscriptionPricingTiers$().subscribe({ + next: (tiers) => { + // Should return empty array due to validation error + expect(tiers).toEqual([]); + expect(logService.error).toHaveBeenCalled(); + expect(toastService.showToast).toHaveBeenCalledWith({ + variant: "error", + title: "", + message: "An unexpected error has occurred.", + }); + done(); + }, + error: () => { + fail("Observable should not error, it should return empty array"); + }, + }); + }); + }); + describe("Observable behavior and caching", () => { it("should share API response between multiple subscriptions", () => { - const getPlansResponse = jest.spyOn(apiService, "getPlans"); + const getPlansResponse = jest.spyOn(billingApiService, "getPlans"); // Subscribe to multiple observables service.getPersonalSubscriptionPricingTiers$().subscribe(); @@ -883,5 +1040,67 @@ describe("SubscriptionPricingService", () => { // API should only be called once due to shareReplay expect(getPlansResponse).toHaveBeenCalledTimes(1); }); + + it("should share premium plan API response between multiple subscriptions when feature flag is enabled", () => { + // Create a new mock to avoid conflicts with beforeEach setup + const newBillingApiService = mock(); + const newConfigService = mock(); + + newBillingApiService.getPlans.mockResolvedValue(mockPlansResponse); + newBillingApiService.getPremiumPlan.mockResolvedValue(mockPremiumPlanResponse); + newConfigService.getFeatureFlag$.mockReturnValue(of(true)); + + const getPremiumPlanSpy = jest.spyOn(newBillingApiService, "getPremiumPlan"); + + // Create a new service instance with the feature flag enabled + const newService = new SubscriptionPricingService( + newBillingApiService, + newConfigService, + i18nService, + logService, + toastService, + ); + + // Subscribe to the premium pricing tier multiple times + newService.getPersonalSubscriptionPricingTiers$().subscribe(); + newService.getPersonalSubscriptionPricingTiers$().subscribe(); + + // API should only be called once due to shareReplay on premiumPlanResponse$ + expect(getPremiumPlanSpy).toHaveBeenCalledTimes(1); + }); + + it("should use hardcoded premium price when feature flag is disabled", (done) => { + // Create a new mock to test from scratch + const newBillingApiService = mock(); + const newConfigService = mock(); + + newBillingApiService.getPlans.mockResolvedValue(mockPlansResponse); + newBillingApiService.getPremiumPlan.mockResolvedValue({ + seat: { price: 999 }, // Different price to verify hardcoded value is used + storage: { price: 999 }, + } as PremiumPlanResponse); + newConfigService.getFeatureFlag$.mockReturnValue(of(false)); + + // Create a new service instance with the feature flag disabled + const newService = new SubscriptionPricingService( + newBillingApiService, + newConfigService, + i18nService, + logService, + toastService, + ); + + // Subscribe with feature flag disabled + newService.getPersonalSubscriptionPricingTiers$().subscribe((tiers) => { + const premiumTier = tiers.find( + (tier) => tier.id === PersonalSubscriptionPricingTierIds.Premium, + ); + + // Should use hardcoded value of 10, not the API response value of 999 + expect(premiumTier!.passwordManager.annualPrice).toBe(10); + expect(premiumTier!.passwordManager.annualPricePerAdditionalStorageGB).toBe(4); + done(); + }); + }); }); }); diff --git a/apps/web/src/app/billing/services/subscription-pricing.service.ts b/apps/web/src/app/billing/services/subscription-pricing.service.ts index 82ec9f180b9..71729a42d23 100644 --- a/apps/web/src/app/billing/services/subscription-pricing.service.ts +++ b/apps/web/src/app/billing/services/subscription-pricing.service.ts @@ -1,11 +1,14 @@ import { Injectable } from "@angular/core"; -import { combineLatest, from, map, Observable, of, shareReplay } from "rxjs"; +import { combineLatest, from, map, Observable, of, shareReplay, switchMap, take } from "rxjs"; import { catchError } from "rxjs/operators"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { PlanType } from "@bitwarden/common/billing/enums"; import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; +import { PremiumPlanResponse } from "@bitwarden/common/billing/models/response/premium-plan.response"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ToastService } from "@bitwarden/components"; import { LogService } from "@bitwarden/logging"; @@ -20,8 +23,18 @@ import { @Injectable({ providedIn: BillingServicesModule }) export class SubscriptionPricingService { + /** + * Fallback premium pricing used when the feature flag is disabled. + * These values represent the legacy pricing model and will not reflect + * server-side price changes. They are retained for backward compatibility + * during the feature flag rollout period. + */ + private static readonly FALLBACK_PREMIUM_SEAT_PRICE = 10; + private static readonly FALLBACK_PREMIUM_STORAGE_PRICE = 4; + constructor( - private apiService: ApiService, + private billingApiService: BillingApiServiceAbstraction, + private configService: ConfigService, private i18nService: I18nService, private logService: LogService, private toastService: ToastService, @@ -55,34 +68,56 @@ export class SubscriptionPricingService { ); private plansResponse$: Observable> = from( - this.apiService.getPlans(), + this.billingApiService.getPlans(), ).pipe(shareReplay({ bufferSize: 1, refCount: false })); - private premium$: Observable = of({ - // premium plan is not configured server-side so for now, hardcode it - basePrice: 10, - additionalStoragePricePerGb: 4, - }).pipe( - map((details) => ({ - id: PersonalSubscriptionPricingTierIds.Premium, - name: this.i18nService.t("premium"), - description: this.i18nService.t("planDescPremium"), - availableCadences: [SubscriptionCadenceIds.Annually], - passwordManager: { - type: "standalone", - annualPrice: details.basePrice, - annualPricePerAdditionalStorageGB: details.additionalStoragePricePerGb, - features: [ - this.featureTranslations.builtInAuthenticator(), - this.featureTranslations.secureFileStorage(), - this.featureTranslations.emergencyAccess(), - this.featureTranslations.breachMonitoring(), - this.featureTranslations.andMoreFeatures(), - ], - }, - })), + private premiumPlanResponse$: Observable = from( + this.billingApiService.getPremiumPlan(), + ).pipe( + catchError((error: unknown) => { + this.logService.error("Failed to fetch premium plan from API", error); + throw error; // Re-throw to propagate to higher-level error handler + }), + shareReplay({ bufferSize: 1, refCount: false }), ); + private premium$: Observable = this.configService + .getFeatureFlag$(FeatureFlag.PM26793_FetchPremiumPriceFromPricingService) + .pipe( + take(1), // Lock behavior at first subscription to prevent switching data sources mid-stream + switchMap((fetchPremiumFromPricingService) => + fetchPremiumFromPricingService + ? this.premiumPlanResponse$.pipe( + map((premiumPlan) => ({ + seat: premiumPlan.seat.price, + storage: premiumPlan.storage.price, + })), + ) + : of({ + seat: SubscriptionPricingService.FALLBACK_PREMIUM_SEAT_PRICE, + storage: SubscriptionPricingService.FALLBACK_PREMIUM_STORAGE_PRICE, + }), + ), + map((premiumPrices) => ({ + id: PersonalSubscriptionPricingTierIds.Premium, + name: this.i18nService.t("premium"), + description: this.i18nService.t("planDescPremium"), + availableCadences: [SubscriptionCadenceIds.Annually], + passwordManager: { + type: "standalone", + annualPrice: premiumPrices.seat, + annualPricePerAdditionalStorageGB: premiumPrices.storage, + features: [ + this.featureTranslations.builtInAuthenticator(), + this.featureTranslations.secureFileStorage(), + this.featureTranslations.emergencyAccess(), + this.featureTranslations.breachMonitoring(), + this.featureTranslations.andMoreFeatures(), + ], + }, + })), + ); + private families$: Observable = this.plansResponse$.pipe( map((plans) => { const familiesPlan = plans.data.find((plan) => plan.type === PlanType.FamiliesAnnually)!; diff --git a/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts b/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts index d581fdaa95c..ef01c98ecb5 100644 --- a/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts @@ -1,3 +1,5 @@ +import { PremiumPlanResponse } from "@bitwarden/common/billing/models/response/premium-plan.response"; + import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request"; import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request"; import { OrganizationBillingMetadataResponse } from "../../billing/models/response/organization-billing-metadata.response"; @@ -25,6 +27,8 @@ export abstract class BillingApiServiceAbstraction { abstract getPlans(): Promise>; + abstract getPremiumPlan(): Promise; + abstract getProviderClientInvoiceReport(providerId: string, invoiceId: string): Promise; abstract getProviderInvoices(providerId: string): Promise; diff --git a/libs/common/src/billing/models/response/premium-plan.response.ts b/libs/common/src/billing/models/response/premium-plan.response.ts new file mode 100644 index 00000000000..f5df560a601 --- /dev/null +++ b/libs/common/src/billing/models/response/premium-plan.response.ts @@ -0,0 +1,47 @@ +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; + +export class PremiumPlanResponse extends BaseResponse { + seat: { + stripePriceId: string; + price: number; + }; + storage: { + stripePriceId: string; + price: number; + }; + + constructor(response: any) { + super(response); + + const seat = this.getResponseProperty("Seat"); + if (!seat || typeof seat !== "object") { + throw new Error("PremiumPlanResponse: Missing or invalid 'Seat' property"); + } + this.seat = new PurchasableResponse(seat); + + const storage = this.getResponseProperty("Storage"); + if (!storage || typeof storage !== "object") { + throw new Error("PremiumPlanResponse: Missing or invalid 'Storage' property"); + } + this.storage = new PurchasableResponse(storage); + } +} + +class PurchasableResponse extends BaseResponse { + stripePriceId: string; + price: number; + + constructor(response: any) { + super(response); + + this.stripePriceId = this.getResponseProperty("StripePriceId"); + if (!this.stripePriceId || typeof this.stripePriceId !== "string") { + throw new Error("PurchasableResponse: Missing or invalid 'StripePriceId' property"); + } + + this.price = this.getResponseProperty("Price"); + if (typeof this.price !== "number" || isNaN(this.price)) { + throw new Error("PurchasableResponse: Missing or invalid 'Price' property"); + } + } +} diff --git a/libs/common/src/billing/services/billing-api.service.ts b/libs/common/src/billing/services/billing-api.service.ts index 165ebf5c3b4..673d4a9784e 100644 --- a/libs/common/src/billing/services/billing-api.service.ts +++ b/libs/common/src/billing/services/billing-api.service.ts @@ -1,6 +1,8 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { PremiumPlanResponse } from "@bitwarden/common/billing/models/response/premium-plan.response"; + import { ApiService } from "../../abstractions/api.service"; import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request"; import { ListResponse } from "../../models/response/list.response"; @@ -61,10 +63,15 @@ export class BillingApiService implements BillingApiServiceAbstraction { } async getPlans(): Promise> { - const r = await this.apiService.send("GET", "/plans", null, false, true); + const r = await this.apiService.send("GET", "/plans", null, true, true); return new ListResponse(r, PlanResponse); } + async getPremiumPlan(): Promise { + const response = await this.apiService.send("GET", "/plans/premium", null, true, true); + return new PremiumPlanResponse(response); + } + async getProviderClientInvoiceReport(providerId: string, invoiceId: string): Promise { const response = await this.apiService.send( "GET", diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index e26fa69fa91..d9cd1dbfab3 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -30,6 +30,7 @@ export enum FeatureFlag { PM25379_UseNewOrganizationMetadataStructure = "pm-25379-use-new-organization-metadata-structure", PM24996_ImplementUpgradeFromFreeDialog = "pm-24996-implement-upgrade-from-free-dialog", PM24033PremiumUpgradeNewDesign = "pm-24033-updat-premium-subscription-page", + PM26793_FetchPremiumPriceFromPricingService = "pm-26793-fetch-premium-price-from-pricing-service", /* Key Management */ PrivateKeyRegeneration = "pm-12241-private-key-regeneration", @@ -115,6 +116,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.PM25379_UseNewOrganizationMetadataStructure]: FALSE, [FeatureFlag.PM24996_ImplementUpgradeFromFreeDialog]: FALSE, [FeatureFlag.PM24033PremiumUpgradeNewDesign]: FALSE, + [FeatureFlag.PM26793_FetchPremiumPriceFromPricingService]: FALSE, /* Key Management */ [FeatureFlag.PrivateKeyRegeneration]: FALSE, From 0691583b5059bc473065720a9930572b95f886f8 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Thu, 23 Oct 2025 11:16:17 -0400 Subject: [PATCH 08/73] [PM-23133] refactor members component (#16703) * WIP: added new services, refactor members to use billing service and member action service * replace dialog logic and user logic with service implementations * WIP * wip add tests * add tests, continue refactoring * clean up * move BillingConstraintService to billing ownership * fix import * fix seat count not updating if feature flag is disabled * refactor billingMetadata, clean up --- .../common/base-members.component.ts | 57 +- .../members/members.component.html | 9 +- .../members/members.component.ts | 738 ++++-------------- .../organizations/members/members.module.ts | 12 + .../organizations/members/services/index.ts | 5 + .../member-actions.service.spec.ts | 463 +++++++++++ .../member-actions/member-actions.service.ts | 210 +++++ .../member-dialog-manager.service.spec.ts | 640 +++++++++++++++ .../member-dialog-manager.service.ts | 322 ++++++++ .../organization-members.service.spec.ts | 362 +++++++++ .../organization-members.service.ts | 76 ++ .../billing-constraint.service.spec.ts | 461 +++++++++++ .../billing-constraint.service.ts | 192 +++++ .../providers/manage/members.component.ts | 38 +- .../organization-metadata.service.spec.ts | 7 +- .../organization-metadata.service.ts | 73 +- 16 files changed, 2999 insertions(+), 666 deletions(-) create mode 100644 apps/web/src/app/admin-console/organizations/members/services/index.ts create mode 100644 apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.spec.ts create mode 100644 apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.ts create mode 100644 apps/web/src/app/admin-console/organizations/members/services/member-dialog-manager/member-dialog-manager.service.spec.ts create mode 100644 apps/web/src/app/admin-console/organizations/members/services/member-dialog-manager/member-dialog-manager.service.ts create mode 100644 apps/web/src/app/admin-console/organizations/members/services/organization-members-service/organization-members.service.spec.ts create mode 100644 apps/web/src/app/admin-console/organizations/members/services/organization-members-service/organization-members.service.ts create mode 100644 apps/web/src/app/billing/members/billing-constraint/billing-constraint.service.spec.ts create mode 100644 apps/web/src/app/billing/members/billing-constraint/billing-constraint.service.ts diff --git a/apps/web/src/app/admin-console/common/base-members.component.ts b/apps/web/src/app/admin-console/common/base-members.component.ts index 21c52949254..5ecf4269a1a 100644 --- a/apps/web/src/app/admin-console/common/base-members.component.ts +++ b/apps/web/src/app/admin-console/common/base-members.component.ts @@ -24,6 +24,7 @@ import { KeyService } from "@bitwarden/key-management"; import { OrganizationUserView } from "../organizations/core/views/organization-user.view"; import { UserConfirmComponent } from "../organizations/manage/user-confirm.component"; +import { MemberActionResult } from "../organizations/members/services/member-actions/member-actions.service"; import { PeopleTableDataSource, peopleFilter } from "./people-table-data-source"; @@ -75,7 +76,7 @@ export abstract class BaseMembersComponent { /** * The currently executing promise - used to avoid multiple user actions executing at once. */ - actionPromise?: Promise; + actionPromise?: Promise; protected searchControl = new FormControl("", { nonNullable: true }); protected statusToggle = new BehaviorSubject(undefined); @@ -101,13 +102,13 @@ export abstract class BaseMembersComponent { abstract edit(user: UserView, organization?: Organization): void; abstract getUsers(organization?: Organization): Promise | UserView[]>; - abstract removeUser(id: string, organization?: Organization): Promise; - abstract reinviteUser(id: string, organization?: Organization): Promise; + abstract removeUser(id: string, organization?: Organization): Promise; + abstract reinviteUser(id: string, organization?: Organization): Promise; abstract confirmUser( user: UserView, publicKey: Uint8Array, organization?: Organization, - ): Promise; + ): Promise; abstract invite(organization?: Organization): void; async load(organization?: Organization) { @@ -140,12 +141,16 @@ export abstract class BaseMembersComponent { this.actionPromise = this.removeUser(user.id, organization); try { - await this.actionPromise; - this.toastService.showToast({ - variant: "success", - message: this.i18nService.t("removedUserId", this.userNamePipe.transform(user)), - }); - this.dataSource.removeUser(user); + const result = await this.actionPromise; + if (result.success) { + this.toastService.showToast({ + variant: "success", + message: this.i18nService.t("removedUserId", this.userNamePipe.transform(user)), + }); + this.dataSource.removeUser(user); + } else { + throw new Error(result.error); + } } catch (e) { this.validationService.showError(e); } @@ -159,11 +164,15 @@ export abstract class BaseMembersComponent { this.actionPromise = this.reinviteUser(user.id, organization); try { - await this.actionPromise; - this.toastService.showToast({ - variant: "success", - message: this.i18nService.t("hasBeenReinvited", this.userNamePipe.transform(user)), - }); + const result = await this.actionPromise; + if (result.success) { + this.toastService.showToast({ + variant: "success", + message: this.i18nService.t("hasBeenReinvited", this.userNamePipe.transform(user)), + }); + } else { + throw new Error(result.error); + } } catch (e) { this.validationService.showError(e); } @@ -174,14 +183,18 @@ export abstract class BaseMembersComponent { const confirmUser = async (publicKey: Uint8Array) => { try { this.actionPromise = this.confirmUser(user, publicKey, organization); - await this.actionPromise; - user.status = this.userStatusType.Confirmed; - this.dataSource.replaceUser(user); + const result = await this.actionPromise; + if (result.success) { + user.status = this.userStatusType.Confirmed; + this.dataSource.replaceUser(user); - this.toastService.showToast({ - variant: "success", - message: this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(user)), - }); + this.toastService.showToast({ + variant: "success", + message: this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(user)), + }); + } else { + throw new Error(result.error); + } } catch (e) { this.validationService.showError(e); throw e; diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.html b/apps/web/src/app/admin-console/organizations/members/members.component.html index 282291eb60e..9401a88ab76 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.html +++ b/apps/web/src/app/admin-console/organizations/members/members.component.html @@ -2,7 +2,7 @@ @if (organization) { @@ -339,7 +339,10 @@ > {{ "userUsingTwoStep" | i18n }} - + @let resetPasswordPolicyEnabled = resetPasswordPolicyEnabled$ | async; + {{ "recoverAccount" | i18n }} 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 3841f6d5b4b..324452499dc 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 @@ -1,14 +1,12 @@ import { Component, computed, Signal } from "@angular/core"; import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop"; -import { ActivatedRoute, Router } from "@angular/router"; +import { ActivatedRoute } from "@angular/router"; import { - BehaviorSubject, combineLatest, concatMap, filter, firstValueFrom, from, - lastValueFrom, map, merge, Observable, @@ -17,24 +15,12 @@ import { take, } from "rxjs"; -import { - OrganizationUserApiService, - OrganizationUserConfirmRequest, - OrganizationUserUserDetailsResponse, - CollectionService, - CollectionData, - Collection, - CollectionDetailsResponse, -} from "@bitwarden/admin-console/common"; +import { OrganizationUserUserDetailsResponse } from "@bitwarden/admin-console/common"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { - getOrganizationById, - OrganizationService, -} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; -import { PolicyApiServiceAbstraction as PolicyApiService } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; +import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { OrganizationUserStatusType, @@ -43,53 +29,32 @@ import { } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; -import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { OrganizationMetadataServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-metadata.service.abstraction"; -import { isNotSelfUpgradable, ProductTierType } from "@bitwarden/common/billing/enums"; import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.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 { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components"; +import { getById } from "@bitwarden/common/platform/misc"; +import { DialogService, ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; +import { UserId } from "@bitwarden/user-core"; +import { BillingConstraintService } from "@bitwarden/web-vault/app/billing/members/billing-constraint/billing-constraint.service"; import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services"; -import { - ChangePlanDialogResultType, - openChangePlanDialog, -} from "../../../billing/organizations/change-plan-dialog.component"; import { BaseMembersComponent } from "../../common/base-members.component"; import { PeopleTableDataSource } from "../../common/people-table-data-source"; -import { GroupApiService } from "../core"; import { OrganizationUserView } from "../core/views/organization-user.view"; -import { openEntityEventsDialog } from "../manage/entity-events.component"; -import { - AccountRecoveryDialogComponent, - AccountRecoveryDialogResultType, -} from "./components/account-recovery/account-recovery-dialog.component"; -import { BulkConfirmDialogComponent } from "./components/bulk/bulk-confirm-dialog.component"; -import { BulkDeleteDialogComponent } from "./components/bulk/bulk-delete-dialog.component"; -import { BulkEnableSecretsManagerDialogComponent } from "./components/bulk/bulk-enable-sm-dialog.component"; -import { BulkRemoveDialogComponent } from "./components/bulk/bulk-remove-dialog.component"; -import { BulkRestoreRevokeComponent } from "./components/bulk/bulk-restore-revoke.component"; -import { BulkStatusComponent } from "./components/bulk/bulk-status.component"; -import { - MemberDialogResult, - MemberDialogTab, - openUserAddEditDialog, -} from "./components/member-dialog"; -import { isFixedSeatPlan } from "./components/member-dialog/validators/org-seat-limit-reached.validator"; +import { AccountRecoveryDialogResultType } from "./components/account-recovery/account-recovery-dialog.component"; +import { MemberDialogResult, MemberDialogTab } from "./components/member-dialog"; +import { MemberDialogManagerService, OrganizationMembersService } from "./services"; import { DeleteManagedMemberWarningService } from "./services/delete-managed-member/delete-managed-member-warning.service"; -import { OrganizationUserService } from "./services/organization-user/organization-user.service"; +import { + MemberActionsService, + MemberActionResult, +} from "./services/member-actions/member-actions.service"; class MembersTableDataSource extends PeopleTableDataSource { protected statusType = OrganizationUserStatusType; @@ -107,7 +72,10 @@ export class MembersComponent extends BaseMembersComponent readonly organization: Signal; status: OrganizationUserStatusType | undefined; - orgResetPasswordPolicyEnabled = false; + + private userId$: Observable = this.accountService.activeAccount$.pipe(getUserId); + + resetPasswordPolicyEnabled$: Observable; protected readonly canUseSecretsManager: Signal = computed( () => this.organization()?.useSecretsManager ?? false, @@ -115,43 +83,34 @@ export class MembersComponent extends BaseMembersComponent protected readonly showUserManagementControls: Signal = computed( () => this.organization()?.canManageUsers ?? false, ); - private refreshBillingMetadata$: BehaviorSubject = new BehaviorSubject(null); protected billingMetadata$: Observable; // Fixed sizes used for cdkVirtualScroll protected rowHeight = 66; protected rowHeightClass = `tw-h-[66px]`; - private userId$: Observable = this.accountService.activeAccount$.pipe(getUserId); - constructor( apiService: ApiService, i18nService: I18nService, organizationManagementPreferencesService: OrganizationManagementPreferencesService, keyService: KeyService, - private encryptService: EncryptService, validationService: ValidationService, logService: LogService, userNamePipe: UserNamePipe, dialogService: DialogService, toastService: ToastService, - private policyService: PolicyService, - private policyApiService: PolicyApiService, private route: ActivatedRoute, - private syncService: SyncService, + protected deleteManagedMemberWarningService: DeleteManagedMemberWarningService, + private organizationWarningsService: OrganizationWarningsService, + private memberActionsService: MemberActionsService, + private memberDialogManager: MemberDialogManagerService, + protected billingConstraint: BillingConstraintService, + protected memberService: OrganizationMembersService, private organizationService: OrganizationService, private accountService: AccountService, - private organizationApiService: OrganizationApiServiceAbstraction, - private organizationUserApiService: OrganizationUserApiService, - private router: Router, - private groupService: GroupApiService, - private collectionService: CollectionService, - private billingApiService: BillingApiServiceAbstraction, + private policyService: PolicyService, + private policyApiService: PolicyApiServiceAbstraction, private organizationMetadataService: OrganizationMetadataServiceAbstraction, - protected deleteManagedMemberWarningService: DeleteManagedMemberWarningService, - private configService: ConfigService, - private organizationUserService: OrganizationUserService, - private organizationWarningsService: OrganizationWarningsService, ) { super( apiService, @@ -169,14 +128,12 @@ export class MembersComponent extends BaseMembersComponent concatMap((params) => this.userId$.pipe( switchMap((userId) => - this.organizationService - .organizations$(userId) - .pipe(getOrganizationById(params.organizationId)), + this.organizationService.organizations$(userId).pipe(getById(params.organizationId)), ), + filter((organization): organization is Organization => organization != null), + shareReplay({ refCount: true, bufferSize: 1 }), ), ), - filter((organization): organization is Organization => organization != null), - shareReplay({ refCount: true, bufferSize: 1 }), ); this.organization = toSignal(organization$); @@ -191,53 +148,26 @@ export class MembersComponent extends BaseMembersComponent ), ); - combineLatest([this.route.queryParams, policies$, organization$]) - .pipe( - concatMap(async ([qParams, policies, organization]) => { - // Backfill pub/priv key if necessary - if (organization.canManageUsersPassword && !organization.hasPublicAndPrivateKeys) { - const orgShareKey = await firstValueFrom( - this.userId$.pipe( - switchMap((userId) => this.keyService.orgKeys$(userId)), - map((orgKeys) => { - if (orgKeys == null || orgKeys[organization.id] == null) { - throw new Error("Organization keys not found for provided User."); - } - return orgKeys[organization.id]; - }), - ), - ); - - const [orgPublicKey, encryptedOrgPrivateKey] = - await this.keyService.makeKeyPair(orgShareKey); - if (encryptedOrgPrivateKey.encryptedString == null) { - throw new Error("Encrypted private key is null."); - } - const request = new OrganizationKeysRequest( - orgPublicKey, - encryptedOrgPrivateKey.encryptedString, - ); - const response = await this.organizationApiService.updateKeys(organization.id, request); - if (response != null) { - await this.syncService.fullSync(true); // Replace organizations with new data - } else { - throw new Error(this.i18nService.t("resetPasswordOrgKeysError")); - } - } - - const resetPasswordPolicy = policies + this.resetPasswordPolicyEnabled$ = combineLatest([organization$, policies$]).pipe( + map( + ([organization, policies]) => + policies .filter((policy) => policy.type === PolicyType.ResetPassword) - .find((p) => p.organizationId === organization.id); - this.orgResetPasswordPolicyEnabled = resetPasswordPolicy?.enabled ?? false; + .find((p) => p.organizationId === organization.id)?.enabled ?? false, + ), + ); - await this.load(organization); + combineLatest([this.route.queryParams, organization$]) + .pipe( + concatMap(async ([qParams, organization]) => { + await this.load(organization!); this.searchControl.setValue(qParams.search); if (qParams.viewEvents != null) { const user = this.dataSource.data.filter((u) => u.id === qParams.viewEvents); if (user.length > 0 && user[0].status === OrganizationUserStatusType.Confirmed) { - this.openEventsDialog(user[0], organization); + this.openEventsDialog(user[0], organization!); } } }), @@ -257,11 +187,10 @@ export class MembersComponent extends BaseMembersComponent ) .subscribe(); - this.billingMetadata$ = combineLatest([this.refreshBillingMetadata$, organization$]).pipe( - switchMap(([_, organization]) => + this.billingMetadata$ = organization$.pipe( + switchMap((organization) => this.organizationMetadataService.getOrganizationMetadata$(organization.id), ), - takeUntilDestroyed(), shareReplay({ bufferSize: 1, refCount: false }), ); @@ -271,136 +200,35 @@ export class MembersComponent extends BaseMembersComponent } override async load(organization: Organization) { - this.refreshBillingMetadata$.next(null); await super.load(organization); } async getUsers(organization: Organization): Promise { - let groupsPromise: Promise> | undefined; - let collectionsPromise: Promise> | undefined; - - // We don't need both groups and collections for the table, so only load one - const userPromise = this.organizationUserApiService.getAllUsers(organization.id, { - includeGroups: organization.useGroups, - includeCollections: !organization.useGroups, - }); - - // Depending on which column is displayed, we need to load the group/collection names - if (organization.useGroups) { - groupsPromise = this.getGroupNameMap(organization); - } else { - collectionsPromise = this.getCollectionNameMap(organization); - } - - const [usersResponse, groupNamesMap, collectionNamesMap] = await Promise.all([ - userPromise, - groupsPromise, - collectionsPromise, - ]); - - return ( - usersResponse.data?.map((r) => { - const userView = OrganizationUserView.fromResponse(r); - - userView.groupNames = userView.groups - .map((g) => groupNamesMap?.get(g)) - .filter((name): name is string => name != null) - .sort(this.i18nService.collator?.compare); - userView.collectionNames = userView.collections - .map((c) => collectionNamesMap?.get(c.id)) - .filter((name): name is string => name != null) - .sort(this.i18nService.collator?.compare); - - return userView; - }) ?? [] - ); + return await this.memberService.loadUsers(organization); } - async getGroupNameMap(organization: Organization): Promise> { - const groups = await this.groupService.getAll(organization.id); - const groupNameMap = new Map(); - groups.forEach((g) => groupNameMap.set(g.id, g.name)); - return groupNameMap; + async removeUser(id: string, organization: Organization): Promise { + return await this.memberActionsService.removeUser(organization, id); } - /** - * Retrieve a map of all collection IDs <-> names for the organization. - */ - async getCollectionNameMap(organization: Organization) { - const response = from(this.apiService.getCollections(organization.id)).pipe( - map((res) => - res.data.map((r) => - Collection.fromCollectionData(new CollectionData(r as CollectionDetailsResponse)), - ), - ), - ); - - const decryptedCollections$ = combineLatest([ - this.userId$.pipe( - switchMap((userId) => this.keyService.orgKeys$(userId)), - filter((orgKeys) => orgKeys != null), - ), - response, - ]).pipe( - switchMap(([orgKeys, collections]) => - this.collectionService.decryptMany$(collections, orgKeys), - ), - map((collections) => { - const collectionMap = new Map(); - collections.forEach((c) => collectionMap.set(c.id, c.name)); - return collectionMap; - }), - ); - - return await firstValueFrom(decryptedCollections$); + async revokeUser(id: string, organization: Organization): Promise { + return await this.memberActionsService.revokeUser(organization, id); } - removeUser(id: string, organization: Organization): Promise { - return this.organizationUserApiService.removeOrganizationUser(organization.id, id); + async restoreUser(id: string, organization: Organization): Promise { + return await this.memberActionsService.restoreUser(organization, id); } - revokeUser(id: string, organization: Organization): Promise { - return this.organizationUserApiService.revokeOrganizationUser(organization.id, id); - } - - restoreUser(id: string, organization: Organization): Promise { - return this.organizationUserApiService.restoreOrganizationUser(organization.id, id); - } - - reinviteUser(id: string, organization: Organization): Promise { - return this.organizationUserApiService.postOrganizationUserReinvite(organization.id, id); + async reinviteUser(id: string, organization: Organization): Promise { + return await this.memberActionsService.reinviteUser(organization, id); } async confirmUser( user: OrganizationUserView, publicKey: Uint8Array, organization: Organization, - ): Promise { - if ( - await firstValueFrom(this.configService.getFeatureFlag$(FeatureFlag.CreateDefaultLocation)) - ) { - await firstValueFrom(this.organizationUserService.confirmUser(organization, user, publicKey)); - } else { - const request = await firstValueFrom( - this.userId$.pipe( - switchMap((userId) => this.keyService.orgKeys$(userId)), - filter((orgKeys) => orgKeys != null), - map((orgKeys) => orgKeys[organization.id]), - switchMap((orgKey) => this.encryptService.encapsulateKeyUnsigned(orgKey, publicKey)), - map((encKey) => { - const req = new OrganizationUserConfirmRequest(); - req.key = encKey.encryptedString; - return req; - }), - ), - ); - - await this.organizationUserApiService.postOrganizationUserConfirm( - organization.id, - user.id, - request, - ); - } + ): Promise { + return await this.memberActionsService.confirmUser(user, publicKey, organization); } async revoke(user: OrganizationUserView, organization: Organization) { @@ -412,12 +240,16 @@ export class MembersComponent extends BaseMembersComponent this.actionPromise = this.revokeUser(user.id, organization); try { - await this.actionPromise; - this.toastService.showToast({ - variant: "success", - message: this.i18nService.t("revokedUserId", this.userNamePipe.transform(user)), - }); - await this.load(organization); + const result = await this.actionPromise; + if (result.success) { + this.toastService.showToast({ + variant: "success", + message: this.i18nService.t("revokedUserId", this.userNamePipe.transform(user)), + }); + await this.load(organization); + } else { + throw new Error(result.error); + } } catch (e) { this.validationService.showError(e); } @@ -427,198 +259,68 @@ export class MembersComponent extends BaseMembersComponent async restore(user: OrganizationUserView, organization: Organization) { this.actionPromise = this.restoreUser(user.id, organization); try { - await this.actionPromise; - this.toastService.showToast({ - variant: "success", - message: this.i18nService.t("restoredUserId", this.userNamePipe.transform(user)), - }); - await this.load(organization); + const result = await this.actionPromise; + if (result.success) { + this.toastService.showToast({ + variant: "success", + message: this.i18nService.t("restoredUserId", this.userNamePipe.transform(user)), + }); + await this.load(organization); + } else { + throw new Error(result.error); + } } catch (e) { this.validationService.showError(e); } this.actionPromise = undefined; } - allowResetPassword(orgUser: OrganizationUserView, organization: Organization): boolean { - let callingUserHasPermission = false; - - switch (organization.type) { - case OrganizationUserType.Owner: - callingUserHasPermission = true; - break; - case OrganizationUserType.Admin: - callingUserHasPermission = orgUser.type !== OrganizationUserType.Owner; - break; - case OrganizationUserType.Custom: - callingUserHasPermission = - orgUser.type !== OrganizationUserType.Owner && - orgUser.type !== OrganizationUserType.Admin; - break; - } - - return ( - organization.canManageUsersPassword && - callingUserHasPermission && - organization.useResetPassword && - organization.hasPublicAndPrivateKeys && - orgUser.resetPasswordEnrolled && - this.orgResetPasswordPolicyEnabled && - orgUser.status === OrganizationUserStatusType.Confirmed + allowResetPassword( + orgUser: OrganizationUserView, + organization: Organization, + orgResetPasswordPolicyEnabled: boolean, + ): boolean { + return this.memberActionsService.allowResetPassword( + orgUser, + organization, + orgResetPasswordPolicyEnabled, ); } showEnrolledStatus( orgUser: OrganizationUserUserDetailsResponse, organization: Organization, + orgResetPasswordPolicyEnabled: boolean, ): boolean { return ( organization.useResetPassword && orgUser.resetPasswordEnrolled && - this.orgResetPasswordPolicyEnabled - ); - } - - private getManageBillingText(organization: Organization): string { - return organization.canEditSubscription ? "ManageBilling" : "NoManageBilling"; - } - - private getProductKey(organization: Organization): string { - let product = ""; - switch (organization.productTierType) { - case ProductTierType.Free: - product = "freeOrg"; - break; - case ProductTierType.TeamsStarter: - product = "teamsStarterPlan"; - break; - case ProductTierType.Families: - product = "familiesPlan"; - break; - default: - throw new Error(`Unsupported product type: ${organization.productTierType}`); - } - return `${product}InvLimitReached${this.getManageBillingText(organization)}`; - } - - private getDialogContent(organization: Organization): string { - return this.i18nService.t(this.getProductKey(organization), organization.seats); - } - - private getAcceptButtonText(organization: Organization): string { - if (!organization.canEditSubscription) { - return this.i18nService.t("ok"); - } - - const productType = organization.productTierType; - - if (isNotSelfUpgradable(productType)) { - throw new Error(`Unsupported product type: ${productType}`); - } - - return this.i18nService.t("upgrade"); - } - - private async handleDialogClose( - result: boolean | undefined, - organization: Organization, - ): Promise { - if (!result || !organization.canEditSubscription) { - return; - } - - const productType = organization.productTierType; - - if (isNotSelfUpgradable(productType)) { - throw new Error(`Unsupported product type: ${organization.productTierType}`); - } - - await this.router.navigate(["/organizations", organization.id, "billing", "subscription"], { - queryParams: { upgrade: true }, - }); - } - - private async showSeatLimitReachedDialog(organization: Organization): Promise { - const orgUpgradeSimpleDialogOpts: SimpleDialogOptions = { - title: this.i18nService.t("upgradeOrganization"), - content: this.getDialogContent(organization), - type: "primary", - acceptButtonText: this.getAcceptButtonText(organization), - }; - - if (!organization.canEditSubscription) { - orgUpgradeSimpleDialogOpts.cancelButtonText = null; - } - - const simpleDialog = this.dialogService.openSimpleDialogRef(orgUpgradeSimpleDialogOpts); - await lastValueFrom( - simpleDialog.closed.pipe(map((closed) => this.handleDialogClose(closed, organization))), + orgResetPasswordPolicyEnabled ); } private async handleInviteDialog(organization: Organization) { const billingMetadata = await firstValueFrom(this.billingMetadata$); - const dialog = openUserAddEditDialog(this.dialogService, { - data: { - kind: "Add", - organizationId: organization.id, - allOrganizationUserEmails: this.dataSource.data?.map((user) => user.email) ?? [], - occupiedSeatCount: billingMetadata?.organizationOccupiedSeats ?? 0, - isOnSecretsManagerStandalone: billingMetadata?.isOnSecretsManagerStandalone ?? false, - }, - }); + const allUserEmails = this.dataSource.data?.map((user) => user.email) ?? []; - const result = await lastValueFrom(dialog.closed); + const result = await this.memberDialogManager.openInviteDialog( + organization, + billingMetadata, + allUserEmails, + ); if (result === MemberDialogResult.Saved) { await this.load(organization); } } - private async handleSeatLimitForFixedTiers(organization: Organization) { - if (!organization.canEditSubscription) { - await this.showSeatLimitReachedDialog(organization); - return; - } - - const reference = openChangePlanDialog(this.dialogService, { - data: { - organizationId: organization.id, - productTierType: organization.productTierType, - }, - }); - - const result = await lastValueFrom(reference.closed); - - if (result === ChangePlanDialogResultType.Submitted) { - await this.load(organization); - } - } - async invite(organization: Organization) { const billingMetadata = await firstValueFrom(this.billingMetadata$); - if ( - organization.hasReseller && - organization.seats === billingMetadata?.organizationOccupiedSeats - ) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("seatLimitReached"), - message: this.i18nService.t("contactYourProvider"), - }); - - return; + const seatLimitResult = this.billingConstraint.checkSeatLimit(organization, billingMetadata); + if (!(await this.billingConstraint.seatLimitReached(seatLimitResult, organization))) { + await this.handleInviteDialog(organization); + this.organizationMetadataService.refreshMetadataCache(); } - - if ( - billingMetadata?.organizationOccupiedSeats === organization.seats && - isFixedSeatPlan(organization.productTierType) - ) { - await this.handleSeatLimitForFixedTiers(organization); - - return; - } - - await this.handleInviteDialog(organization); } async edit( @@ -627,20 +329,14 @@ export class MembersComponent extends BaseMembersComponent initialTab: MemberDialogTab = MemberDialogTab.Role, ) { const billingMetadata = await firstValueFrom(this.billingMetadata$); - const dialog = openUserAddEditDialog(this.dialogService, { - data: { - kind: "Edit", - name: this.userNamePipe.transform(user), - organizationId: organization.id, - organizationUserId: user.id, - usesKeyConnector: user.usesKeyConnector, - isOnSecretsManagerStandalone: billingMetadata?.isOnSecretsManagerStandalone ?? false, - initialTab: initialTab, - managedByOrganization: user.managedByOrganization, - }, - }); - const result = await lastValueFrom(dialog.closed); + const result = await this.memberDialogManager.openEditDialog( + user, + organization, + billingMetadata, + initialTab, + ); + switch (result) { case MemberDialogResult.Deleted: this.dataSource.removeUser(user); @@ -658,43 +354,23 @@ export class MembersComponent extends BaseMembersComponent return; } - const dialogRef = BulkRemoveDialogComponent.open(this.dialogService, { - data: { - organizationId: organization.id, - users: this.dataSource.getCheckedUsers(), - }, - }); - await lastValueFrom(dialogRef.closed); + await this.memberDialogManager.openBulkRemoveDialog( + organization, + this.dataSource.getCheckedUsers(), + ); + this.organizationMetadataService.refreshMetadataCache(); await this.load(organization); } async bulkDelete(organization: Organization) { - const warningAcknowledged = await firstValueFrom( - this.deleteManagedMemberWarningService.warningAcknowledged(organization.id), - ); - - if ( - !warningAcknowledged && - organization.canManageUsers && - organization.productTierType === ProductTierType.Enterprise - ) { - const acknowledged = await this.deleteManagedMemberWarningService.showWarning(); - if (!acknowledged) { - return; - } - } - if (this.actionPromise != null) { return; } - const dialogRef = BulkDeleteDialogComponent.open(this.dialogService, { - data: { - organizationId: organization.id, - users: this.dataSource.getCheckedUsers(), - }, - }); - await lastValueFrom(dialogRef.closed); + await this.memberDialogManager.openBulkDeleteDialog( + organization, + this.dataSource.getCheckedUsers(), + ); await this.load(organization); } @@ -711,13 +387,11 @@ export class MembersComponent extends BaseMembersComponent return; } - const ref = BulkRestoreRevokeComponent.open(this.dialogService, { - organizationId: organization.id, - users: this.dataSource.getCheckedUsers(), - isRevoking: isRevoking, - }); - - await firstValueFrom(ref.closed); + await this.memberDialogManager.openBulkRestoreRevokeDialog( + organization, + this.dataSource.getCheckedUsers(), + isRevoking, + ); await this.load(organization); } @@ -739,20 +413,22 @@ export class MembersComponent extends BaseMembersComponent } try { - const response = this.organizationUserApiService.postManyOrganizationUserReinvite( - organization.id, + const result = await this.memberActionsService.bulkReinvite( + organization, filteredUsers.map((user) => user.id), ); + + if (!result.successful) { + throw new Error(); + } + // Bulk Status component open - const dialogRef = BulkStatusComponent.open(this.dialogService, { - data: { - users: users, - filteredUsers: filteredUsers, - request: response, - successfulMessage: this.i18nService.t("bulkReinviteMessage"), - }, - }); - await lastValueFrom(dialogRef.closed); + await this.memberDialogManager.openBulkStatusDialog( + users, + filteredUsers, + Promise.resolve(result.successful), + this.i18nService.t("bulkReinviteMessage"), + ); } catch (e) { this.validationService.showError(e); } @@ -764,49 +440,24 @@ export class MembersComponent extends BaseMembersComponent return; } - const dialogRef = BulkConfirmDialogComponent.open(this.dialogService, { - data: { - organization: organization, - users: this.dataSource.getCheckedUsers(), - }, - }); - - await lastValueFrom(dialogRef.closed); + await this.memberDialogManager.openBulkConfirmDialog( + organization, + this.dataSource.getCheckedUsers(), + ); await this.load(organization); } async bulkEnableSM(organization: Organization) { - const users = this.dataSource.getCheckedUsers().filter((ou) => !ou.accessSecretsManager); + const users = this.dataSource.getCheckedUsers(); - if (users.length === 0) { - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorOccurred"), - message: this.i18nService.t("noSelectedUsersApplicable"), - }); - return; - } + await this.memberDialogManager.openBulkEnableSecretsManagerDialog(organization, users); - const dialogRef = BulkEnableSecretsManagerDialogComponent.open(this.dialogService, { - orgId: organization.id, - users, - }); - - await lastValueFrom(dialogRef.closed); this.dataSource.uncheckAllUsers(); await this.load(organization); } openEventsDialog(user: OrganizationUserView, organization: Organization) { - openEntityEventsDialog(this.dialogService, { - data: { - name: this.userNamePipe.transform(user), - organizationId: organization.id, - entityId: user.id, - showUser: false, - entity: "user", - }, - }); + this.memberDialogManager.openEventsDialog(user, organization); } async resetPassword(user: OrganizationUserView, organization: Organization) { @@ -821,16 +472,7 @@ export class MembersComponent extends BaseMembersComponent return; } - const dialogRef = AccountRecoveryDialogComponent.open(this.dialogService, { - data: { - name: this.userNamePipe.transform(user), - email: user.email, - organizationId: organization.id as OrganizationId, - organizationUserId: user.id, - }, - }); - - const result = await lastValueFrom(dialogRef.closed); + const result = await this.memberDialogManager.openAccountRecoveryDialog(user, organization); if (result === AccountRecoveryDialogResultType.Ok) { await this.load(organization); } @@ -839,91 +481,29 @@ export class MembersComponent extends BaseMembersComponent } protected async removeUserConfirmationDialog(user: OrganizationUserView) { - const content = user.usesKeyConnector - ? "removeUserConfirmationKeyConnector" - : "removeOrgUserConfirmation"; - - const confirmed = await this.dialogService.openSimpleDialog({ - title: { - key: "removeUserIdAccess", - placeholders: [this.userNamePipe.transform(user)], - }, - content: { key: content }, - type: "warning", - }); - - if (!confirmed) { - return false; - } - - if (user.status > OrganizationUserStatusType.Invited && user.hasMasterPassword === false) { - return await this.noMasterPasswordConfirmationDialog(user); - } - - return true; + return await this.memberDialogManager.openRemoveUserConfirmationDialog(user); } protected async revokeUserConfirmationDialog(user: OrganizationUserView) { - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "revokeAccess", placeholders: [this.userNamePipe.transform(user)] }, - content: this.i18nService.t("revokeUserConfirmation"), - acceptButtonText: { key: "revokeAccess" }, - type: "warning", - }); - - if (!confirmed) { - return false; - } - - if (user.status > OrganizationUserStatusType.Invited && user.hasMasterPassword === false) { - return await this.noMasterPasswordConfirmationDialog(user); - } - - return true; + return await this.memberDialogManager.openRevokeUserConfirmationDialog(user); } async deleteUser(user: OrganizationUserView, organization: Organization) { - const warningAcknowledged = await firstValueFrom( - this.deleteManagedMemberWarningService.warningAcknowledged(organization.id), + const confirmed = await this.memberDialogManager.openDeleteUserConfirmationDialog( + user, + organization, ); - if ( - !warningAcknowledged && - organization.canManageUsers && - organization.productTierType === ProductTierType.Enterprise - ) { - const acknowledged = await this.deleteManagedMemberWarningService.showWarning(); - if (!acknowledged) { - return false; - } - } - - const confirmed = await this.dialogService.openSimpleDialog({ - title: { - key: "deleteOrganizationUser", - placeholders: [this.userNamePipe.transform(user)], - }, - content: { - key: "deleteOrganizationUserWarningDesc", - placeholders: [this.userNamePipe.transform(user)], - }, - type: "warning", - acceptButtonText: { key: "delete" }, - cancelButtonText: { key: "cancel" }, - }); - if (!confirmed) { return false; } - await this.deleteManagedMemberWarningService.acknowledgeWarning(organization.id); - - this.actionPromise = this.organizationUserApiService.deleteOrganizationUser( - organization.id, - user.id, - ); + this.actionPromise = this.memberActionsService.deleteUser(organization, user.id); try { - await this.actionPromise; + const result = await this.actionPromise; + if (!result.success) { + throw new Error(result.error); + } this.toastService.showToast({ variant: "success", message: this.i18nService.t("organizationUserDeleted", this.userNamePipe.transform(user)), @@ -935,19 +515,6 @@ export class MembersComponent extends BaseMembersComponent this.actionPromise = undefined; } - private async noMasterPasswordConfirmationDialog(user: OrganizationUserView) { - return this.dialogService.openSimpleDialog({ - title: { - key: "removeOrgUserNoMasterPasswordTitle", - }, - content: { - key: "removeOrgUserNoMasterPasswordDesc", - placeholders: [this.userNamePipe.transform(user)], - }, - type: "warning", - }); - } - get showBulkRestoreUsers(): boolean { return this.dataSource .getCheckedUsers() @@ -975,13 +542,4 @@ export class MembersComponent extends BaseMembersComponent .getCheckedUsers() .every((member) => member.managedByOrganization && validStatuses.includes(member.status)); } - - async navigateToPaymentMethod(organization: Organization) { - await this.router.navigate( - ["organizations", `${organization.id}`, "billing", "payment-details"], - { - state: { launchPaymentModalAutomatically: true }, - }, - ); - } } diff --git a/apps/web/src/app/admin-console/organizations/members/members.module.ts b/apps/web/src/app/admin-console/organizations/members/members.module.ts index e5bc5f29a3b..3b233932ed3 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.module.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.module.ts @@ -4,6 +4,7 @@ import { NgModule } from "@angular/core"; import { PasswordStrengthV2Component } from "@bitwarden/angular/tools/password-strength/password-strength-v2.component"; import { PasswordCalloutComponent } from "@bitwarden/auth/angular"; import { ScrollLayoutDirective } from "@bitwarden/components"; +import { BillingConstraintService } from "@bitwarden/web-vault/app/billing/members/billing-constraint/billing-constraint.service"; import { OrganizationFreeTrialWarningComponent } from "@bitwarden/web-vault/app/billing/organizations/warnings/components"; import { HeaderModule } from "../../../layouts/header/header.module"; @@ -18,6 +19,11 @@ import { BulkStatusComponent } from "./components/bulk/bulk-status.component"; import { UserDialogModule } from "./components/member-dialog"; import { MembersRoutingModule } from "./members-routing.module"; import { MembersComponent } from "./members.component"; +import { + OrganizationMembersService, + MemberActionsService, + MemberDialogManagerService, +} from "./services"; @NgModule({ imports: [ @@ -40,5 +46,11 @@ import { MembersComponent } from "./members.component"; MembersComponent, BulkDeleteDialogComponent, ], + providers: [ + OrganizationMembersService, + MemberActionsService, + BillingConstraintService, + MemberDialogManagerService, + ], }) export class MembersModule {} diff --git a/apps/web/src/app/admin-console/organizations/members/services/index.ts b/apps/web/src/app/admin-console/organizations/members/services/index.ts new file mode 100644 index 00000000000..2ac2d31cd69 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/services/index.ts @@ -0,0 +1,5 @@ +export { OrganizationMembersService } from "./organization-members-service/organization-members.service"; +export { MemberActionsService } from "./member-actions/member-actions.service"; +export { MemberDialogManagerService } from "./member-dialog-manager/member-dialog-manager.service"; +export { DeleteManagedMemberWarningService } from "./delete-managed-member/delete-managed-member-warning.service"; +export { OrganizationUserService } from "./organization-user/organization-user.service"; diff --git a/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.spec.ts b/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.spec.ts new file mode 100644 index 00000000000..6fd7de7b292 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.spec.ts @@ -0,0 +1,463 @@ +import { MockProxy, mock } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { + OrganizationUserApiService, + OrganizationUserBulkResponse, +} from "@bitwarden/admin-console/common"; +import { + OrganizationUserType, + OrganizationUserStatusType, +} from "@bitwarden/common/admin-console/enums"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; +import { OrgKey } from "@bitwarden/common/types/key"; +import { newGuid } from "@bitwarden/guid"; +import { KeyService } from "@bitwarden/key-management"; + +import { BillingConstraintService } from "../../../../../billing/members/billing-constraint/billing-constraint.service"; +import { OrganizationUserView } from "../../../core/views/organization-user.view"; +import { OrganizationUserService } from "../organization-user/organization-user.service"; + +import { MemberActionsService } from "./member-actions.service"; + +describe("MemberActionsService", () => { + let service: MemberActionsService; + let organizationUserApiService: MockProxy; + let organizationUserService: MockProxy; + let keyService: MockProxy; + let encryptService: MockProxy; + let configService: MockProxy; + let accountService: FakeAccountService; + let billingConstraintService: MockProxy; + + const userId = newGuid() as UserId; + const organizationId = newGuid() as OrganizationId; + const userIdToManage = newGuid(); + + let mockOrganization: Organization; + let mockOrgUser: OrganizationUserView; + + beforeEach(() => { + organizationUserApiService = mock(); + organizationUserService = mock(); + keyService = mock(); + encryptService = mock(); + configService = mock(); + accountService = mockAccountServiceWith(userId); + billingConstraintService = mock(); + + mockOrganization = { + id: organizationId, + type: OrganizationUserType.Owner, + canManageUsersPassword: true, + hasPublicAndPrivateKeys: true, + useResetPassword: true, + } as Organization; + + mockOrgUser = { + id: userIdToManage, + userId: userIdToManage, + type: OrganizationUserType.User, + status: OrganizationUserStatusType.Confirmed, + resetPasswordEnrolled: true, + } as OrganizationUserView; + + service = new MemberActionsService( + organizationUserApiService, + organizationUserService, + keyService, + encryptService, + configService, + accountService, + billingConstraintService, + ); + }); + + describe("inviteUser", () => { + it("should successfully invite a user", async () => { + organizationUserApiService.postOrganizationUserInvite.mockResolvedValue(undefined); + + const result = await service.inviteUser( + mockOrganization, + "test@example.com", + OrganizationUserType.User, + {}, + [], + [], + ); + + expect(result).toEqual({ success: true }); + expect(organizationUserApiService.postOrganizationUserInvite).toHaveBeenCalledWith( + organizationId, + { + emails: ["test@example.com"], + type: OrganizationUserType.User, + accessSecretsManager: false, + collections: [], + groups: [], + permissions: {}, + }, + ); + }); + + it("should handle invite errors", async () => { + const errorMessage = "Invitation failed"; + organizationUserApiService.postOrganizationUserInvite.mockRejectedValue( + new Error(errorMessage), + ); + + const result = await service.inviteUser( + mockOrganization, + "test@example.com", + OrganizationUserType.User, + ); + + expect(result).toEqual({ success: false, error: errorMessage }); + }); + }); + + describe("removeUser", () => { + it("should successfully remove a user", async () => { + organizationUserApiService.removeOrganizationUser.mockResolvedValue(undefined); + + const result = await service.removeUser(mockOrganization, userIdToManage); + + expect(result).toEqual({ success: true }); + expect(organizationUserApiService.removeOrganizationUser).toHaveBeenCalledWith( + organizationId, + userIdToManage, + ); + }); + + it("should handle remove errors", async () => { + const errorMessage = "Remove failed"; + organizationUserApiService.removeOrganizationUser.mockRejectedValue(new Error(errorMessage)); + + const result = await service.removeUser(mockOrganization, userIdToManage); + + expect(result).toEqual({ success: false, error: errorMessage }); + }); + }); + + describe("revokeUser", () => { + it("should successfully revoke a user", async () => { + organizationUserApiService.revokeOrganizationUser.mockResolvedValue(undefined); + + const result = await service.revokeUser(mockOrganization, userIdToManage); + + expect(result).toEqual({ success: true }); + expect(organizationUserApiService.revokeOrganizationUser).toHaveBeenCalledWith( + organizationId, + userIdToManage, + ); + }); + + it("should handle revoke errors", async () => { + const errorMessage = "Revoke failed"; + organizationUserApiService.revokeOrganizationUser.mockRejectedValue(new Error(errorMessage)); + + const result = await service.revokeUser(mockOrganization, userIdToManage); + + expect(result).toEqual({ success: false, error: errorMessage }); + }); + }); + + describe("restoreUser", () => { + it("should successfully restore a user", async () => { + organizationUserApiService.restoreOrganizationUser.mockResolvedValue(undefined); + + const result = await service.restoreUser(mockOrganization, userIdToManage); + + expect(result).toEqual({ success: true }); + expect(organizationUserApiService.restoreOrganizationUser).toHaveBeenCalledWith( + organizationId, + userIdToManage, + ); + }); + + it("should handle restore errors", async () => { + const errorMessage = "Restore failed"; + organizationUserApiService.restoreOrganizationUser.mockRejectedValue(new Error(errorMessage)); + + const result = await service.restoreUser(mockOrganization, userIdToManage); + + expect(result).toEqual({ success: false, error: errorMessage }); + }); + }); + + describe("deleteUser", () => { + it("should successfully delete a user", async () => { + organizationUserApiService.deleteOrganizationUser.mockResolvedValue(undefined); + + const result = await service.deleteUser(mockOrganization, userIdToManage); + + expect(result).toEqual({ success: true }); + expect(organizationUserApiService.deleteOrganizationUser).toHaveBeenCalledWith( + organizationId, + userIdToManage, + ); + }); + + it("should handle delete errors", async () => { + const errorMessage = "Delete failed"; + organizationUserApiService.deleteOrganizationUser.mockRejectedValue(new Error(errorMessage)); + + const result = await service.deleteUser(mockOrganization, userIdToManage); + + expect(result).toEqual({ success: false, error: errorMessage }); + }); + }); + + describe("reinviteUser", () => { + it("should successfully reinvite a user", async () => { + organizationUserApiService.postOrganizationUserReinvite.mockResolvedValue(undefined); + + const result = await service.reinviteUser(mockOrganization, userIdToManage); + + expect(result).toEqual({ success: true }); + expect(organizationUserApiService.postOrganizationUserReinvite).toHaveBeenCalledWith( + organizationId, + userIdToManage, + ); + }); + + it("should handle reinvite errors", async () => { + const errorMessage = "Reinvite failed"; + organizationUserApiService.postOrganizationUserReinvite.mockRejectedValue( + new Error(errorMessage), + ); + + const result = await service.reinviteUser(mockOrganization, userIdToManage); + + expect(result).toEqual({ success: false, error: errorMessage }); + }); + }); + + describe("confirmUser", () => { + const publicKey = new Uint8Array([1, 2, 3, 4, 5]); + + it("should confirm user using new flow when feature flag is enabled", async () => { + configService.getFeatureFlag$.mockReturnValue(of(true)); + organizationUserService.confirmUser.mockReturnValue(of(undefined)); + + const result = await service.confirmUser(mockOrgUser, publicKey, mockOrganization); + + expect(result).toEqual({ success: true }); + expect(organizationUserService.confirmUser).toHaveBeenCalledWith( + mockOrganization, + mockOrgUser, + publicKey, + ); + expect(organizationUserApiService.postOrganizationUserConfirm).not.toHaveBeenCalled(); + }); + + it("should confirm user using exising flow when feature flag is disabled", async () => { + configService.getFeatureFlag$.mockReturnValue(of(false)); + + const mockOrgKey = mock(); + const mockOrgKeys = { [organizationId]: mockOrgKey }; + keyService.orgKeys$.mockReturnValue(of(mockOrgKeys)); + + const mockEncryptedKey = new EncString("encrypted-key-data"); + encryptService.encapsulateKeyUnsigned.mockResolvedValue(mockEncryptedKey); + + organizationUserApiService.postOrganizationUserConfirm.mockResolvedValue(undefined); + + const result = await service.confirmUser(mockOrgUser, publicKey, mockOrganization); + + expect(result).toEqual({ success: true }); + expect(keyService.orgKeys$).toHaveBeenCalledWith(userId); + expect(encryptService.encapsulateKeyUnsigned).toHaveBeenCalledWith(mockOrgKey, publicKey); + expect(organizationUserApiService.postOrganizationUserConfirm).toHaveBeenCalledWith( + organizationId, + userIdToManage, + expect.objectContaining({ + key: "encrypted-key-data", + }), + ); + }); + + it("should handle missing organization keys", async () => { + configService.getFeatureFlag$.mockReturnValue(of(false)); + keyService.orgKeys$.mockReturnValue(of({})); + + const result = await service.confirmUser(mockOrgUser, publicKey, mockOrganization); + + expect(result.success).toBe(false); + expect(result.error).toContain("Organization keys not found"); + }); + + it("should handle confirm errors", async () => { + configService.getFeatureFlag$.mockReturnValue(of(true)); + const errorMessage = "Confirm failed"; + organizationUserService.confirmUser.mockImplementation(() => { + throw new Error(errorMessage); + }); + + const result = await service.confirmUser(mockOrgUser, publicKey, mockOrganization); + + expect(result.success).toBe(false); + expect(result.error).toContain(errorMessage); + }); + }); + + describe("bulkReinvite", () => { + const userIds = [newGuid(), newGuid(), newGuid()]; + + it("should successfully reinvite multiple users", async () => { + const mockResponse = { + data: userIds.map((id) => ({ + id, + error: null, + })), + continuationToken: null, + } as ListResponse; + organizationUserApiService.postManyOrganizationUserReinvite.mockResolvedValue(mockResponse); + + const result = await service.bulkReinvite(mockOrganization, userIds); + + expect(result).toEqual({ + successful: mockResponse, + failed: [], + }); + expect(organizationUserApiService.postManyOrganizationUserReinvite).toHaveBeenCalledWith( + organizationId, + userIds, + ); + }); + + it("should handle bulk reinvite errors", async () => { + const errorMessage = "Bulk reinvite failed"; + organizationUserApiService.postManyOrganizationUserReinvite.mockRejectedValue( + new Error(errorMessage), + ); + + const result = await service.bulkReinvite(mockOrganization, userIds); + + expect(result.successful).toBeUndefined(); + expect(result.failed).toHaveLength(3); + expect(result.failed[0]).toEqual({ id: userIds[0], error: errorMessage }); + }); + }); + + describe("allowResetPassword", () => { + const resetPasswordEnabled = true; + + it("should allow reset password for Owner over User", () => { + const result = service.allowResetPassword( + mockOrgUser, + mockOrganization, + resetPasswordEnabled, + ); + + expect(result).toBe(true); + }); + + it("should allow reset password for Admin over User", () => { + const adminOrg = { ...mockOrganization, type: OrganizationUserType.Admin } as Organization; + + const result = service.allowResetPassword(mockOrgUser, adminOrg, resetPasswordEnabled); + + expect(result).toBe(true); + }); + + it("should not allow reset password for Admin over Owner", () => { + const adminOrg = { ...mockOrganization, type: OrganizationUserType.Admin } as Organization; + const ownerUser = { + ...mockOrgUser, + type: OrganizationUserType.Owner, + } as OrganizationUserView; + + const result = service.allowResetPassword(ownerUser, adminOrg, resetPasswordEnabled); + + expect(result).toBe(false); + }); + + it("should allow reset password for Custom over User", () => { + const customOrg = { ...mockOrganization, type: OrganizationUserType.Custom } as Organization; + + const result = service.allowResetPassword(mockOrgUser, customOrg, resetPasswordEnabled); + + expect(result).toBe(true); + }); + + it("should not allow reset password for Custom over Admin", () => { + const customOrg = { ...mockOrganization, type: OrganizationUserType.Custom } as Organization; + const adminUser = { + ...mockOrgUser, + type: OrganizationUserType.Admin, + } as OrganizationUserView; + + const result = service.allowResetPassword(adminUser, customOrg, resetPasswordEnabled); + + expect(result).toBe(false); + }); + + it("should not allow reset password for Custom over Owner", () => { + const customOrg = { ...mockOrganization, type: OrganizationUserType.Custom } as Organization; + const ownerUser = { + ...mockOrgUser, + type: OrganizationUserType.Owner, + } as OrganizationUserView; + + const result = service.allowResetPassword(ownerUser, customOrg, resetPasswordEnabled); + + expect(result).toBe(false); + }); + + it("should not allow reset password when organization cannot manage users password", () => { + const org = { ...mockOrganization, canManageUsersPassword: false } as Organization; + + const result = service.allowResetPassword(mockOrgUser, org, resetPasswordEnabled); + + expect(result).toBe(false); + }); + + it("should not allow reset password when organization does not use reset password", () => { + const org = { ...mockOrganization, useResetPassword: false } as Organization; + + const result = service.allowResetPassword(mockOrgUser, org, resetPasswordEnabled); + + expect(result).toBe(false); + }); + + it("should not allow reset password when organization lacks public and private keys", () => { + const org = { ...mockOrganization, hasPublicAndPrivateKeys: false } as Organization; + + const result = service.allowResetPassword(mockOrgUser, org, resetPasswordEnabled); + + expect(result).toBe(false); + }); + + it("should not allow reset password when user is not enrolled in reset password", () => { + const user = { ...mockOrgUser, resetPasswordEnrolled: false } as OrganizationUserView; + + const result = service.allowResetPassword(user, mockOrganization, resetPasswordEnabled); + + expect(result).toBe(false); + }); + + it("should not allow reset password when reset password is disabled", () => { + const result = service.allowResetPassword(mockOrgUser, mockOrganization, false); + + expect(result).toBe(false); + }); + + it("should not allow reset password when user status is not confirmed", () => { + const user = { + ...mockOrgUser, + status: OrganizationUserStatusType.Invited, + } as OrganizationUserView; + + const result = service.allowResetPassword(user, mockOrganization, resetPasswordEnabled); + + expect(result).toBe(false); + }); + }); +}); diff --git a/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.ts b/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.ts new file mode 100644 index 00000000000..3697aba94ff --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.ts @@ -0,0 +1,210 @@ +import { Injectable } from "@angular/core"; +import { firstValueFrom, switchMap, map } from "rxjs"; + +import { + OrganizationUserApiService, + OrganizationUserBulkResponse, + OrganizationUserConfirmRequest, +} from "@bitwarden/admin-console/common"; +import { + OrganizationUserType, + OrganizationUserStatusType, +} 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 { OrganizationMetadataServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-metadata.service.abstraction"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { KeyService } from "@bitwarden/key-management"; + +import { OrganizationUserView } from "../../../core/views/organization-user.view"; +import { OrganizationUserService } from "../organization-user/organization-user.service"; + +export interface MemberActionResult { + success: boolean; + error?: string; +} + +export interface BulkActionResult { + successful?: ListResponse; + failed: { id: string; error: string }[]; +} + +@Injectable() +export class MemberActionsService { + private userId$ = this.accountService.activeAccount$.pipe(getUserId); + + constructor( + private organizationUserApiService: OrganizationUserApiService, + private organizationUserService: OrganizationUserService, + private keyService: KeyService, + private encryptService: EncryptService, + private configService: ConfigService, + private accountService: AccountService, + private organizationMetadataService: OrganizationMetadataServiceAbstraction, + ) {} + + async inviteUser( + organization: Organization, + email: string, + type: OrganizationUserType, + permissions?: any, + collections?: any[], + groups?: string[], + ): Promise { + try { + await this.organizationUserApiService.postOrganizationUserInvite(organization.id, { + emails: [email], + type, + accessSecretsManager: false, + collections: collections ?? [], + groups: groups ?? [], + permissions, + }); + return { success: true }; + } catch (error) { + return { success: false, error: (error as Error).message ?? String(error) }; + } + } + + async removeUser(organization: Organization, userId: string): Promise { + try { + await this.organizationUserApiService.removeOrganizationUser(organization.id, userId); + this.organizationMetadataService.refreshMetadataCache(); + return { success: true }; + } catch (error) { + return { success: false, error: (error as Error).message ?? String(error) }; + } + } + + async revokeUser(organization: Organization, userId: string): Promise { + try { + await this.organizationUserApiService.revokeOrganizationUser(organization.id, userId); + this.organizationMetadataService.refreshMetadataCache(); + return { success: true }; + } catch (error) { + return { success: false, error: (error as Error).message ?? String(error) }; + } + } + + async restoreUser(organization: Organization, userId: string): Promise { + try { + await this.organizationUserApiService.restoreOrganizationUser(organization.id, userId); + this.organizationMetadataService.refreshMetadataCache(); + return { success: true }; + } catch (error) { + return { success: false, error: (error as Error).message ?? String(error) }; + } + } + + async deleteUser(organization: Organization, userId: string): Promise { + try { + await this.organizationUserApiService.deleteOrganizationUser(organization.id, userId); + this.organizationMetadataService.refreshMetadataCache(); + return { success: true }; + } catch (error) { + return { success: false, error: (error as Error).message ?? String(error) }; + } + } + + async reinviteUser(organization: Organization, userId: string): Promise { + try { + await this.organizationUserApiService.postOrganizationUserReinvite(organization.id, userId); + return { success: true }; + } catch (error) { + return { success: false, error: (error as Error).message ?? String(error) }; + } + } + + async confirmUser( + user: OrganizationUserView, + publicKey: Uint8Array, + organization: Organization, + ): Promise { + try { + if ( + await firstValueFrom(this.configService.getFeatureFlag$(FeatureFlag.CreateDefaultLocation)) + ) { + await firstValueFrom( + this.organizationUserService.confirmUser(organization, user, publicKey), + ); + } else { + const request = await firstValueFrom( + this.userId$.pipe( + switchMap((userId) => this.keyService.orgKeys$(userId)), + map((orgKeys) => { + if (orgKeys == null || orgKeys[organization.id] == null) { + throw new Error("Organization keys not found for provided User."); + } + return orgKeys[organization.id]; + }), + switchMap((orgKey) => this.encryptService.encapsulateKeyUnsigned(orgKey, publicKey)), + map((encKey) => { + const req = new OrganizationUserConfirmRequest(); + req.key = encKey.encryptedString; + return req; + }), + ), + ); + + await this.organizationUserApiService.postOrganizationUserConfirm( + organization.id, + user.id, + request, + ); + } + return { success: true }; + } catch (error) { + return { success: false, error: (error as Error).message ?? String(error) }; + } + } + + async bulkReinvite(organization: Organization, userIds: string[]): Promise { + try { + const result = await this.organizationUserApiService.postManyOrganizationUserReinvite( + organization.id, + userIds, + ); + return { successful: result, failed: [] }; + } catch (error) { + return { + failed: userIds.map((id) => ({ id, error: (error as Error).message ?? String(error) })), + }; + } + } + + allowResetPassword( + orgUser: OrganizationUserView, + organization: Organization, + resetPasswordEnabled: boolean, + ): boolean { + let callingUserHasPermission = false; + + switch (organization.type) { + case OrganizationUserType.Owner: + callingUserHasPermission = true; + break; + case OrganizationUserType.Admin: + callingUserHasPermission = orgUser.type !== OrganizationUserType.Owner; + break; + case OrganizationUserType.Custom: + callingUserHasPermission = + orgUser.type !== OrganizationUserType.Owner && + orgUser.type !== OrganizationUserType.Admin; + break; + } + + return ( + organization.canManageUsersPassword && + callingUserHasPermission && + organization.useResetPassword && + organization.hasPublicAndPrivateKeys && + orgUser.resetPasswordEnrolled && + resetPasswordEnabled && + orgUser.status === OrganizationUserStatusType.Confirmed + ); + } +} diff --git a/apps/web/src/app/admin-console/organizations/members/services/member-dialog-manager/member-dialog-manager.service.spec.ts b/apps/web/src/app/admin-console/organizations/members/services/member-dialog-manager/member-dialog-manager.service.spec.ts new file mode 100644 index 00000000000..e478f8bbb41 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/services/member-dialog-manager/member-dialog-manager.service.spec.ts @@ -0,0 +1,640 @@ +import { mock, MockProxy } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; +import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { ProductTierType } from "@bitwarden/common/billing/enums"; +import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogService, ToastService } from "@bitwarden/components"; + +import { OrganizationUserView } from "../../../core/views/organization-user.view"; +import { EntityEventsComponent } from "../../../manage/entity-events.component"; +import { AccountRecoveryDialogComponent } from "../../components/account-recovery/account-recovery-dialog.component"; +import { BulkConfirmDialogComponent } from "../../components/bulk/bulk-confirm-dialog.component"; +import { BulkDeleteDialogComponent } from "../../components/bulk/bulk-delete-dialog.component"; +import { BulkEnableSecretsManagerDialogComponent } from "../../components/bulk/bulk-enable-sm-dialog.component"; +import { BulkRemoveDialogComponent } from "../../components/bulk/bulk-remove-dialog.component"; +import { BulkRestoreRevokeComponent } from "../../components/bulk/bulk-restore-revoke.component"; +import { BulkStatusComponent } from "../../components/bulk/bulk-status.component"; +import { + MemberDialogComponent, + MemberDialogResult, + MemberDialogTab, +} from "../../components/member-dialog"; +import { DeleteManagedMemberWarningService } from "../delete-managed-member/delete-managed-member-warning.service"; + +import { MemberDialogManagerService } from "./member-dialog-manager.service"; + +describe("MemberDialogManagerService", () => { + let service: MemberDialogManagerService; + let dialogService: MockProxy; + let i18nService: MockProxy; + let toastService: MockProxy; + let userNamePipe: MockProxy; + let deleteManagedMemberWarningService: MockProxy; + + let mockOrganization: Organization; + let mockUser: OrganizationUserView; + let mockBillingMetadata: OrganizationBillingMetadataResponse; + + beforeEach(() => { + dialogService = mock(); + i18nService = mock(); + toastService = mock(); + userNamePipe = mock(); + deleteManagedMemberWarningService = mock(); + + service = new MemberDialogManagerService( + dialogService, + i18nService, + toastService, + userNamePipe, + deleteManagedMemberWarningService, + ); + + // Setup mock data + mockOrganization = { + id: "org-id", + canManageUsers: true, + productTierType: ProductTierType.Enterprise, + } as Organization; + + mockUser = { + id: "user-id", + email: "test@example.com", + name: "Test User", + usesKeyConnector: false, + status: OrganizationUserStatusType.Confirmed, + hasMasterPassword: true, + accessSecretsManager: false, + managedByOrganization: false, + } as OrganizationUserView; + + mockBillingMetadata = { + organizationOccupiedSeats: 10, + isOnSecretsManagerStandalone: false, + } as OrganizationBillingMetadataResponse; + + userNamePipe.transform.mockReturnValue("Test User"); + }); + + describe("openInviteDialog", () => { + it("should open the invite dialog with correct parameters", async () => { + const mockDialogRef = { closed: of(MemberDialogResult.Saved) }; + dialogService.open.mockReturnValue(mockDialogRef as any); + + const allUserEmails = ["user1@example.com", "user2@example.com"]; + + const result = await service.openInviteDialog( + mockOrganization, + mockBillingMetadata, + allUserEmails, + ); + + expect(dialogService.open).toHaveBeenCalledWith( + MemberDialogComponent, + expect.objectContaining({ + data: { + kind: "Add", + organizationId: mockOrganization.id, + allOrganizationUserEmails: allUserEmails, + occupiedSeatCount: 10, + isOnSecretsManagerStandalone: false, + }, + }), + ); + expect(result).toBe(MemberDialogResult.Saved); + }); + + it("should return Canceled when dialog is closed without result", async () => { + const mockDialogRef = { closed: of(null) }; + dialogService.open.mockReturnValue(mockDialogRef as any); + + const result = await service.openInviteDialog(mockOrganization, mockBillingMetadata, []); + + expect(result).toBe(MemberDialogResult.Canceled); + }); + + it("should handle null billing metadata", async () => { + const mockDialogRef = { closed: of(MemberDialogResult.Saved) }; + dialogService.open.mockReturnValue(mockDialogRef as any); + + await service.openInviteDialog(mockOrganization, null, []); + + expect(dialogService.open).toHaveBeenCalledWith( + MemberDialogComponent, + expect.objectContaining({ + data: expect.objectContaining({ + occupiedSeatCount: 0, + isOnSecretsManagerStandalone: false, + }), + }), + ); + }); + }); + + describe("openEditDialog", () => { + it("should open the edit dialog with correct parameters", async () => { + const mockDialogRef = { closed: of(MemberDialogResult.Saved) }; + dialogService.open.mockReturnValue(mockDialogRef as any); + + const result = await service.openEditDialog(mockUser, mockOrganization, mockBillingMetadata); + + expect(dialogService.open).toHaveBeenCalledWith( + MemberDialogComponent, + expect.objectContaining({ + data: { + kind: "Edit", + name: "Test User", + organizationId: mockOrganization.id, + organizationUserId: mockUser.id, + usesKeyConnector: false, + isOnSecretsManagerStandalone: false, + initialTab: MemberDialogTab.Role, + managedByOrganization: false, + }, + }), + ); + expect(result).toBe(MemberDialogResult.Saved); + }); + + it("should use custom initial tab when provided", async () => { + const mockDialogRef = { closed: of(MemberDialogResult.Saved) }; + dialogService.open.mockReturnValue(mockDialogRef as any); + + await service.openEditDialog( + mockUser, + mockOrganization, + mockBillingMetadata, + MemberDialogTab.AccountRecovery, + ); + + expect(dialogService.open).toHaveBeenCalledWith( + MemberDialogComponent, + expect.objectContaining({ + data: expect.objectContaining({ + initialTab: 0, // MemberDialogTab.AccountRecovery is 0 + }), + }), + ); + }); + + it("should return Canceled when dialog is closed without result", async () => { + const mockDialogRef = { closed: of(null) }; + dialogService.open.mockReturnValue(mockDialogRef as any); + + const result = await service.openEditDialog(mockUser, mockOrganization, mockBillingMetadata); + + expect(result).toBe(MemberDialogResult.Canceled); + }); + }); + + describe("openAccountRecoveryDialog", () => { + it("should open account recovery dialog with correct parameters", async () => { + const mockDialogRef = { closed: of("recovered") }; + dialogService.open.mockReturnValue(mockDialogRef as any); + + const result = await service.openAccountRecoveryDialog(mockUser, mockOrganization); + + expect(dialogService.open).toHaveBeenCalledWith( + AccountRecoveryDialogComponent, + expect.objectContaining({ + data: { + name: "Test User", + email: mockUser.email, + organizationId: mockOrganization.id, + organizationUserId: mockUser.id, + }, + }), + ); + expect(result).toBe("recovered"); + }); + + it("should return Ok when dialog is closed without result", async () => { + const mockDialogRef = { closed: of(null) }; + dialogService.open.mockReturnValue(mockDialogRef as any); + + const result = await service.openAccountRecoveryDialog(mockUser, mockOrganization); + + expect(result).toBe("ok"); + }); + }); + + describe("openBulkConfirmDialog", () => { + it("should open bulk confirm dialog with correct parameters", async () => { + const mockDialogRef = { closed: of(undefined) }; + dialogService.open.mockReturnValue(mockDialogRef as any); + + const users = [mockUser]; + await service.openBulkConfirmDialog(mockOrganization, users); + + expect(dialogService.open).toHaveBeenCalledWith( + BulkConfirmDialogComponent, + expect.objectContaining({ + data: { + organization: mockOrganization, + users: users, + }, + }), + ); + }); + }); + + describe("openBulkRemoveDialog", () => { + it("should open bulk remove dialog with correct parameters", async () => { + const mockDialogRef = { closed: of(undefined) }; + dialogService.open.mockReturnValue(mockDialogRef as any); + + const users = [mockUser]; + await service.openBulkRemoveDialog(mockOrganization, users); + + expect(dialogService.open).toHaveBeenCalledWith( + BulkRemoveDialogComponent, + expect.objectContaining({ + data: { + organizationId: mockOrganization.id, + users: users, + }, + }), + ); + }); + }); + + describe("openBulkDeleteDialog", () => { + it("should open bulk delete dialog when warning already acknowledged", async () => { + deleteManagedMemberWarningService.warningAcknowledged.mockReturnValue(of(true)); + + const mockDialogRef = { closed: of(undefined) }; + dialogService.open.mockReturnValue(mockDialogRef as any); + + const users = [mockUser]; + await service.openBulkDeleteDialog(mockOrganization, users); + + expect(dialogService.open).toHaveBeenCalledWith( + BulkDeleteDialogComponent, + expect.objectContaining({ + data: { + organizationId: mockOrganization.id, + users: users, + }, + }), + ); + expect(deleteManagedMemberWarningService.showWarning).not.toHaveBeenCalled(); + }); + + it("should show warning before opening dialog for enterprise organizations", async () => { + deleteManagedMemberWarningService.warningAcknowledged.mockReturnValue(of(false)); + deleteManagedMemberWarningService.showWarning.mockResolvedValue(true); + + const mockDialogRef = { closed: of(undefined) }; + dialogService.open.mockReturnValue(mockDialogRef as any); + + const users = [mockUser]; + await service.openBulkDeleteDialog(mockOrganization, users); + + expect(deleteManagedMemberWarningService.showWarning).toHaveBeenCalled(); + expect(dialogService.open).toHaveBeenCalled(); + }); + + it("should not open dialog if warning is not acknowledged", async () => { + deleteManagedMemberWarningService.warningAcknowledged.mockReturnValue(of(false)); + deleteManagedMemberWarningService.showWarning.mockResolvedValue(false); + + const users = [mockUser]; + await service.openBulkDeleteDialog(mockOrganization, users); + + expect(deleteManagedMemberWarningService.showWarning).toHaveBeenCalled(); + expect(dialogService.open).not.toHaveBeenCalled(); + }); + + it("should skip warning for non-enterprise organizations", async () => { + const nonEnterpriseOrg = { + ...mockOrganization, + productTierType: ProductTierType.Free, + } as Organization; + + deleteManagedMemberWarningService.warningAcknowledged.mockReturnValue(of(false)); + + const mockDialogRef = { closed: of(undefined) }; + dialogService.open.mockReturnValue(mockDialogRef as any); + + const users = [mockUser]; + await service.openBulkDeleteDialog(nonEnterpriseOrg, users); + + expect(deleteManagedMemberWarningService.showWarning).not.toHaveBeenCalled(); + expect(dialogService.open).toHaveBeenCalled(); + }); + }); + + describe("openBulkRestoreRevokeDialog", () => { + it("should open bulk restore revoke dialog with correct parameters for revoking", async () => { + const mockDialogRef = { closed: of(undefined) }; + dialogService.open.mockReturnValue(mockDialogRef as any); + + const users = [mockUser]; + await service.openBulkRestoreRevokeDialog(mockOrganization, users, true); + + expect(dialogService.open).toHaveBeenCalledWith( + BulkRestoreRevokeComponent, + expect.objectContaining({ + data: expect.objectContaining({ + organizationId: mockOrganization.id, + users: users, + isRevoking: true, + }), + }), + ); + }); + + it("should open bulk restore revoke dialog with correct parameters for restoring", async () => { + const mockDialogRef = { closed: of(undefined) }; + dialogService.open.mockReturnValue(mockDialogRef as any); + + const users = [mockUser]; + await service.openBulkRestoreRevokeDialog(mockOrganization, users, false); + + expect(dialogService.open).toHaveBeenCalledWith( + BulkRestoreRevokeComponent, + expect.objectContaining({ + data: expect.objectContaining({ + organizationId: mockOrganization.id, + users: users, + isRevoking: false, + }), + }), + ); + }); + }); + + describe("openBulkEnableSecretsManagerDialog", () => { + it("should open dialog with eligible users only", async () => { + const mockDialogRef = { closed: of(undefined) }; + dialogService.open.mockReturnValue(mockDialogRef as any); + + const user1 = { ...mockUser, accessSecretsManager: false } as OrganizationUserView; + const user2 = { + ...mockUser, + id: "user-2", + accessSecretsManager: true, + } as OrganizationUserView; + const users = [user1, user2]; + + await service.openBulkEnableSecretsManagerDialog(mockOrganization, users); + + expect(dialogService.open).toHaveBeenCalledWith( + BulkEnableSecretsManagerDialogComponent, + expect.objectContaining({ + data: expect.objectContaining({ + orgId: mockOrganization.id, + users: [user1], + }), + }), + ); + }); + + it("should show error toast when no eligible users", async () => { + i18nService.t.mockImplementation((key) => key); + + const user1 = { ...mockUser, accessSecretsManager: true } as OrganizationUserView; + const users = [user1]; + + await service.openBulkEnableSecretsManagerDialog(mockOrganization, users); + + expect(toastService.showToast).toHaveBeenCalledWith({ + variant: "error", + title: "errorOccurred", + message: "noSelectedUsersApplicable", + }); + expect(dialogService.open).not.toHaveBeenCalled(); + }); + }); + + describe("openBulkStatusDialog", () => { + it("should open bulk status dialog with correct parameters", async () => { + const mockDialogRef = { closed: of(undefined) }; + dialogService.open.mockReturnValue(mockDialogRef as any); + + const users = [mockUser]; + const filteredUsers = [mockUser]; + const request = Promise.resolve(); + const successMessage = "Success!"; + + await service.openBulkStatusDialog(users, filteredUsers, request, successMessage); + + expect(dialogService.open).toHaveBeenCalledWith( + BulkStatusComponent, + expect.objectContaining({ + data: { + users: users, + filteredUsers: filteredUsers, + request: request, + successfulMessage: successMessage, + }, + }), + ); + }); + }); + + describe("openEventsDialog", () => { + it("should open events dialog with correct parameters", () => { + service.openEventsDialog(mockUser, mockOrganization); + + expect(dialogService.open).toHaveBeenCalledWith( + EntityEventsComponent, + expect.objectContaining({ + data: { + name: "Test User", + organizationId: mockOrganization.id, + entityId: mockUser.id, + showUser: false, + entity: "user", + }, + }), + ); + }); + }); + + describe("openRemoveUserConfirmationDialog", () => { + it("should return true when user confirms removal", async () => { + dialogService.openSimpleDialog.mockResolvedValue(true); + + const result = await service.openRemoveUserConfirmationDialog(mockUser); + + expect(dialogService.openSimpleDialog).toHaveBeenCalledWith({ + title: { + key: "removeUserIdAccess", + placeholders: ["Test User"], + }, + content: { key: "removeOrgUserConfirmation" }, + type: "warning", + }); + expect(result).toBe(true); + }); + + it("should show key connector warning when user uses key connector", async () => { + const keyConnectorUser = { ...mockUser, usesKeyConnector: true } as OrganizationUserView; + dialogService.openSimpleDialog.mockResolvedValue(true); + + await service.openRemoveUserConfirmationDialog(keyConnectorUser); + + expect(dialogService.openSimpleDialog).toHaveBeenCalledWith( + expect.objectContaining({ + content: { key: "removeUserConfirmationKeyConnector" }, + }), + ); + }); + + it("should return false when user cancels confirmation", async () => { + dialogService.openSimpleDialog.mockResolvedValue(false); + + const result = await service.openRemoveUserConfirmationDialog(mockUser); + + expect(result).toBe(false); + }); + + it("should show no master password warning for confirmed users without master password", async () => { + const noMpUser = { + ...mockUser, + status: OrganizationUserStatusType.Confirmed, + hasMasterPassword: false, + } as OrganizationUserView; + + dialogService.openSimpleDialog.mockResolvedValueOnce(true).mockResolvedValueOnce(true); + + const result = await service.openRemoveUserConfirmationDialog(noMpUser); + + expect(dialogService.openSimpleDialog).toHaveBeenCalledTimes(2); + expect(dialogService.openSimpleDialog).toHaveBeenLastCalledWith({ + title: { + key: "removeOrgUserNoMasterPasswordTitle", + }, + content: { + key: "removeOrgUserNoMasterPasswordDesc", + placeholders: ["Test User"], + }, + type: "warning", + }); + expect(result).toBe(true); + }); + }); + + describe("openRevokeUserConfirmationDialog", () => { + it("should return true when user confirms revocation", async () => { + i18nService.t.mockReturnValue("Revoke user confirmation"); + dialogService.openSimpleDialog.mockResolvedValue(true); + + const result = await service.openRevokeUserConfirmationDialog(mockUser); + + expect(dialogService.openSimpleDialog).toHaveBeenCalledWith({ + title: { key: "revokeAccess", placeholders: ["Test User"] }, + content: "Revoke user confirmation", + acceptButtonText: { key: "revokeAccess" }, + type: "warning", + }); + expect(result).toBe(true); + }); + + it("should return false when user cancels confirmation", async () => { + i18nService.t.mockReturnValue("Revoke user confirmation"); + dialogService.openSimpleDialog.mockResolvedValue(false); + + const result = await service.openRevokeUserConfirmationDialog(mockUser); + + expect(result).toBe(false); + }); + + it("should show no master password warning for confirmed users without master password", async () => { + const noMpUser = { + ...mockUser, + status: OrganizationUserStatusType.Confirmed, + hasMasterPassword: false, + } as OrganizationUserView; + + i18nService.t.mockReturnValue("Revoke user confirmation"); + dialogService.openSimpleDialog.mockResolvedValueOnce(true).mockResolvedValueOnce(true); + + const result = await service.openRevokeUserConfirmationDialog(noMpUser); + + expect(dialogService.openSimpleDialog).toHaveBeenCalledTimes(2); + expect(result).toBe(true); + }); + }); + + describe("openDeleteUserConfirmationDialog", () => { + it("should return true when user confirms deletion", async () => { + deleteManagedMemberWarningService.warningAcknowledged.mockReturnValue(of(true)); + dialogService.openSimpleDialog.mockResolvedValue(true); + + const result = await service.openDeleteUserConfirmationDialog(mockUser, mockOrganization); + + expect(dialogService.openSimpleDialog).toHaveBeenCalledWith({ + title: { + key: "deleteOrganizationUser", + placeholders: ["Test User"], + }, + content: { + key: "deleteOrganizationUserWarningDesc", + placeholders: ["Test User"], + }, + type: "warning", + acceptButtonText: { key: "delete" }, + cancelButtonText: { key: "cancel" }, + }); + expect(deleteManagedMemberWarningService.acknowledgeWarning).toHaveBeenCalledWith( + mockOrganization.id, + ); + expect(result).toBe(true); + }); + + it("should show warning before deletion for enterprise organizations", async () => { + deleteManagedMemberWarningService.warningAcknowledged.mockReturnValue(of(false)); + deleteManagedMemberWarningService.showWarning.mockResolvedValue(true); + dialogService.openSimpleDialog.mockResolvedValue(true); + + const result = await service.openDeleteUserConfirmationDialog(mockUser, mockOrganization); + + expect(deleteManagedMemberWarningService.showWarning).toHaveBeenCalled(); + expect(dialogService.openSimpleDialog).toHaveBeenCalled(); + expect(result).toBe(true); + }); + + it("should return false if warning is not acknowledged", async () => { + deleteManagedMemberWarningService.warningAcknowledged.mockReturnValue(of(false)); + deleteManagedMemberWarningService.showWarning.mockResolvedValue(false); + + const result = await service.openDeleteUserConfirmationDialog(mockUser, mockOrganization); + + expect(deleteManagedMemberWarningService.showWarning).toHaveBeenCalled(); + expect(dialogService.openSimpleDialog).not.toHaveBeenCalled(); + expect(result).toBe(false); + }); + + it("should skip warning for non-enterprise organizations", async () => { + const nonEnterpriseOrg = { + ...mockOrganization, + productTierType: ProductTierType.Free, + } as Organization; + + deleteManagedMemberWarningService.warningAcknowledged.mockReturnValue(of(false)); + dialogService.openSimpleDialog.mockResolvedValue(true); + + const result = await service.openDeleteUserConfirmationDialog(mockUser, nonEnterpriseOrg); + + expect(deleteManagedMemberWarningService.showWarning).not.toHaveBeenCalled(); + expect(dialogService.openSimpleDialog).toHaveBeenCalled(); + expect(result).toBe(true); + }); + + it("should return false when user cancels confirmation", async () => { + deleteManagedMemberWarningService.warningAcknowledged.mockReturnValue(of(true)); + dialogService.openSimpleDialog.mockResolvedValue(false); + + const result = await service.openDeleteUserConfirmationDialog(mockUser, mockOrganization); + + expect(result).toBe(false); + expect(deleteManagedMemberWarningService.acknowledgeWarning).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/web/src/app/admin-console/organizations/members/services/member-dialog-manager/member-dialog-manager.service.ts b/apps/web/src/app/admin-console/organizations/members/services/member-dialog-manager/member-dialog-manager.service.ts new file mode 100644 index 00000000000..c6ef536af2b --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/services/member-dialog-manager/member-dialog-manager.service.ts @@ -0,0 +1,322 @@ +import { Injectable } from "@angular/core"; +import { firstValueFrom, lastValueFrom } from "rxjs"; + +import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { ProductTierType } from "@bitwarden/common/billing/enums"; +import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { OrganizationId } from "@bitwarden/common/types/guid"; +import { DialogService, ToastService } from "@bitwarden/components"; + +import { OrganizationUserView } from "../../../core/views/organization-user.view"; +import { openEntityEventsDialog } from "../../../manage/entity-events.component"; +import { + AccountRecoveryDialogComponent, + AccountRecoveryDialogResultType, +} from "../../components/account-recovery/account-recovery-dialog.component"; +import { BulkConfirmDialogComponent } from "../../components/bulk/bulk-confirm-dialog.component"; +import { BulkDeleteDialogComponent } from "../../components/bulk/bulk-delete-dialog.component"; +import { BulkEnableSecretsManagerDialogComponent } from "../../components/bulk/bulk-enable-sm-dialog.component"; +import { BulkRemoveDialogComponent } from "../../components/bulk/bulk-remove-dialog.component"; +import { BulkRestoreRevokeComponent } from "../../components/bulk/bulk-restore-revoke.component"; +import { BulkStatusComponent } from "../../components/bulk/bulk-status.component"; +import { + MemberDialogResult, + MemberDialogTab, + openUserAddEditDialog, +} from "../../components/member-dialog"; +import { DeleteManagedMemberWarningService } from "../delete-managed-member/delete-managed-member-warning.service"; + +@Injectable() +export class MemberDialogManagerService { + constructor( + private dialogService: DialogService, + private i18nService: I18nService, + private toastService: ToastService, + private userNamePipe: UserNamePipe, + private deleteManagedMemberWarningService: DeleteManagedMemberWarningService, + ) {} + + async openInviteDialog( + organization: Organization, + billingMetadata: OrganizationBillingMetadataResponse, + allUserEmails: string[], + ): Promise { + const dialog = openUserAddEditDialog(this.dialogService, { + data: { + kind: "Add", + organizationId: organization.id, + allOrganizationUserEmails: allUserEmails, + occupiedSeatCount: billingMetadata?.organizationOccupiedSeats ?? 0, + isOnSecretsManagerStandalone: billingMetadata?.isOnSecretsManagerStandalone ?? false, + }, + }); + + const result = await lastValueFrom(dialog.closed); + return result ?? MemberDialogResult.Canceled; + } + + async openEditDialog( + user: OrganizationUserView, + organization: Organization, + billingMetadata: OrganizationBillingMetadataResponse, + initialTab: MemberDialogTab = MemberDialogTab.Role, + ): Promise { + const dialog = openUserAddEditDialog(this.dialogService, { + data: { + kind: "Edit", + name: this.userNamePipe.transform(user), + organizationId: organization.id, + organizationUserId: user.id, + usesKeyConnector: user.usesKeyConnector, + isOnSecretsManagerStandalone: billingMetadata?.isOnSecretsManagerStandalone ?? false, + initialTab: initialTab, + managedByOrganization: user.managedByOrganization, + }, + }); + + const result = await lastValueFrom(dialog.closed); + return result ?? MemberDialogResult.Canceled; + } + + async openAccountRecoveryDialog( + user: OrganizationUserView, + organization: Organization, + ): Promise { + const dialogRef = AccountRecoveryDialogComponent.open(this.dialogService, { + data: { + name: this.userNamePipe.transform(user), + email: user.email, + organizationId: organization.id as OrganizationId, + organizationUserId: user.id, + }, + }); + + const result = await lastValueFrom(dialogRef.closed); + return result ?? AccountRecoveryDialogResultType.Ok; + } + + async openBulkConfirmDialog( + organization: Organization, + users: OrganizationUserView[], + ): Promise { + const dialogRef = BulkConfirmDialogComponent.open(this.dialogService, { + data: { + organization: organization, + users: users, + }, + }); + + await lastValueFrom(dialogRef.closed); + } + + async openBulkRemoveDialog( + organization: Organization, + users: OrganizationUserView[], + ): Promise { + const dialogRef = BulkRemoveDialogComponent.open(this.dialogService, { + data: { + organizationId: organization.id, + users: users, + }, + }); + + await lastValueFrom(dialogRef.closed); + } + + async openBulkDeleteDialog( + organization: Organization, + users: OrganizationUserView[], + ): Promise { + const warningAcknowledged = await firstValueFrom( + this.deleteManagedMemberWarningService.warningAcknowledged(organization.id), + ); + + if ( + !warningAcknowledged && + organization.canManageUsers && + organization.productTierType === ProductTierType.Enterprise + ) { + const acknowledged = await this.deleteManagedMemberWarningService.showWarning(); + if (!acknowledged) { + return; + } + } + + const dialogRef = BulkDeleteDialogComponent.open(this.dialogService, { + data: { + organizationId: organization.id, + users: users, + }, + }); + + await lastValueFrom(dialogRef.closed); + } + + async openBulkRestoreRevokeDialog( + organization: Organization, + users: OrganizationUserView[], + isRevoking: boolean, + ): Promise { + const ref = BulkRestoreRevokeComponent.open(this.dialogService, { + organizationId: organization.id, + users: users, + isRevoking: isRevoking, + }); + + await firstValueFrom(ref.closed); + } + + async openBulkEnableSecretsManagerDialog( + organization: Organization, + users: OrganizationUserView[], + ): Promise { + const eligibleUsers = users.filter((ou) => !ou.accessSecretsManager); + + if (eligibleUsers.length === 0) { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("noSelectedUsersApplicable"), + }); + return; + } + + const dialogRef = BulkEnableSecretsManagerDialogComponent.open(this.dialogService, { + orgId: organization.id, + users: eligibleUsers, + }); + + await lastValueFrom(dialogRef.closed); + } + + async openBulkStatusDialog( + users: OrganizationUserView[], + filteredUsers: OrganizationUserView[], + request: Promise, + successMessage: string, + ): Promise { + const dialogRef = BulkStatusComponent.open(this.dialogService, { + data: { + users: users, + filteredUsers: filteredUsers, + request: request, + successfulMessage: successMessage, + }, + }); + + await lastValueFrom(dialogRef.closed); + } + + openEventsDialog(user: OrganizationUserView, organization: Organization): void { + openEntityEventsDialog(this.dialogService, { + data: { + name: this.userNamePipe.transform(user), + organizationId: organization.id, + entityId: user.id, + showUser: false, + entity: "user", + }, + }); + } + + async openRemoveUserConfirmationDialog(user: OrganizationUserView): Promise { + const content = user.usesKeyConnector + ? "removeUserConfirmationKeyConnector" + : "removeOrgUserConfirmation"; + + const confirmed = await this.dialogService.openSimpleDialog({ + title: { + key: "removeUserIdAccess", + placeholders: [this.userNamePipe.transform(user)], + }, + content: { key: content }, + type: "warning", + }); + + if (!confirmed) { + return false; + } + + if (user.status > 0 && user.hasMasterPassword === false) { + return await this.openNoMasterPasswordConfirmationDialog(user); + } + + return true; + } + + async openRevokeUserConfirmationDialog(user: OrganizationUserView): Promise { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "revokeAccess", placeholders: [this.userNamePipe.transform(user)] }, + content: this.i18nService.t("revokeUserConfirmation"), + acceptButtonText: { key: "revokeAccess" }, + type: "warning", + }); + + if (!confirmed) { + return false; + } + + if (user.status > 0 && user.hasMasterPassword === false) { + return await this.openNoMasterPasswordConfirmationDialog(user); + } + + return true; + } + + async openDeleteUserConfirmationDialog( + user: OrganizationUserView, + organization: Organization, + ): Promise { + const warningAcknowledged = await firstValueFrom( + this.deleteManagedMemberWarningService.warningAcknowledged(organization.id), + ); + + if ( + !warningAcknowledged && + organization.canManageUsers && + organization.productTierType === ProductTierType.Enterprise + ) { + const acknowledged = await this.deleteManagedMemberWarningService.showWarning(); + if (!acknowledged) { + return false; + } + } + + const confirmed = await this.dialogService.openSimpleDialog({ + title: { + key: "deleteOrganizationUser", + placeholders: [this.userNamePipe.transform(user)], + }, + content: { + key: "deleteOrganizationUserWarningDesc", + placeholders: [this.userNamePipe.transform(user)], + }, + type: "warning", + acceptButtonText: { key: "delete" }, + cancelButtonText: { key: "cancel" }, + }); + + if (confirmed) { + await this.deleteManagedMemberWarningService.acknowledgeWarning(organization.id); + } + + return confirmed; + } + + private async openNoMasterPasswordConfirmationDialog( + user: OrganizationUserView, + ): Promise { + return this.dialogService.openSimpleDialog({ + title: { + key: "removeOrgUserNoMasterPasswordTitle", + }, + content: { + key: "removeOrgUserNoMasterPasswordDesc", + placeholders: [this.userNamePipe.transform(user)], + }, + type: "warning", + }); + } +} diff --git a/apps/web/src/app/admin-console/organizations/members/services/organization-members-service/organization-members.service.spec.ts b/apps/web/src/app/admin-console/organizations/members/services/organization-members-service/organization-members.service.spec.ts new file mode 100644 index 00000000000..615d2ece463 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/services/organization-members-service/organization-members.service.spec.ts @@ -0,0 +1,362 @@ +import { TestBed } from "@angular/core/testing"; + +import { + OrganizationUserApiService, + OrganizationUserUserDetailsResponse, +} from "@bitwarden/admin-console/common"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { OrganizationId } from "@bitwarden/common/types/guid"; + +import { GroupApiService } from "../../../core"; + +import { OrganizationMembersService } from "./organization-members.service"; + +describe("OrganizationMembersService", () => { + let service: OrganizationMembersService; + let organizationUserApiService: jest.Mocked; + let groupService: jest.Mocked; + let apiService: jest.Mocked; + + const mockOrganizationId = "org-123" as OrganizationId; + + const createMockOrganization = (overrides: Partial = {}): Organization => { + const org = new Organization(); + org.id = mockOrganizationId; + org.useGroups = false; + + return Object.assign(org, overrides); + }; + + const createMockUserResponse = ( + overrides: Partial = {}, + ): OrganizationUserUserDetailsResponse => { + return { + id: "user-1", + userId: "user-id-1", + email: "test@example.com", + name: "Test User", + collections: [], + groups: [], + ...overrides, + } as OrganizationUserUserDetailsResponse; + }; + + const createMockGroup = (id: string, name: string) => ({ + id, + name, + }); + + const createMockCollection = (id: string, name: string) => ({ + id, + name, + }); + + beforeEach(() => { + organizationUserApiService = { + getAllUsers: jest.fn(), + } as any; + + groupService = { + getAll: jest.fn(), + } as any; + + apiService = { + getCollections: jest.fn(), + } as any; + + TestBed.configureTestingModule({ + providers: [ + OrganizationMembersService, + { provide: OrganizationUserApiService, useValue: organizationUserApiService }, + { provide: GroupApiService, useValue: groupService }, + { provide: ApiService, useValue: apiService }, + ], + }); + + service = TestBed.inject(OrganizationMembersService); + }); + + describe("loadUsers", () => { + it("should load users with collections when organization does not use groups", async () => { + const organization = createMockOrganization({ useGroups: false }); + const mockUser = createMockUserResponse({ + collections: [{ id: "col-1" } as any], + }); + const mockUsersResponse: ListResponse = { + data: [mockUser], + } as any; + const mockCollections = [createMockCollection("col-1", "Collection 1")]; + + organizationUserApiService.getAllUsers.mockResolvedValue(mockUsersResponse); + apiService.getCollections.mockResolvedValue({ + data: mockCollections, + } as any); + + const result = await service.loadUsers(organization); + + expect(organizationUserApiService.getAllUsers).toHaveBeenCalledWith(mockOrganizationId, { + includeGroups: false, + includeCollections: true, + }); + expect(apiService.getCollections).toHaveBeenCalledWith(mockOrganizationId); + expect(groupService.getAll).not.toHaveBeenCalled(); + expect(result).toHaveLength(1); + expect(result[0].collectionNames).toEqual(["Collection 1"]); + expect(result[0].groupNames).toEqual([]); + }); + + it("should load users with groups when organization uses groups", async () => { + const organization = createMockOrganization({ useGroups: true }); + const mockUser = createMockUserResponse({ + groups: ["group-1", "group-2"], + }); + const mockUsersResponse: ListResponse = { + data: [mockUser], + } as any; + const mockGroups = [ + createMockGroup("group-1", "Group 1"), + createMockGroup("group-2", "Group 2"), + ]; + + organizationUserApiService.getAllUsers.mockResolvedValue(mockUsersResponse); + groupService.getAll.mockResolvedValue(mockGroups as any); + + const result = await service.loadUsers(organization); + + expect(organizationUserApiService.getAllUsers).toHaveBeenCalledWith(mockOrganizationId, { + includeGroups: true, + includeCollections: false, + }); + expect(groupService.getAll).toHaveBeenCalledWith(mockOrganizationId); + expect(apiService.getCollections).not.toHaveBeenCalled(); + expect(result).toHaveLength(1); + expect(result[0].groupNames).toEqual(["Group 1", "Group 2"]); + expect(result[0].collectionNames).toEqual([]); + }); + + it("should sort group names alphabetically", async () => { + const organization = createMockOrganization({ useGroups: true }); + const mockUser = createMockUserResponse({ + groups: ["group-1", "group-2", "group-3"], + }); + const mockUsersResponse: ListResponse = { + data: [mockUser], + } as any; + const mockGroups = [ + createMockGroup("group-1", "Zebra Group"), + createMockGroup("group-2", "Alpha Group"), + createMockGroup("group-3", "Beta Group"), + ]; + + organizationUserApiService.getAllUsers.mockResolvedValue(mockUsersResponse); + groupService.getAll.mockResolvedValue(mockGroups as any); + + const result = await service.loadUsers(organization); + + expect(result[0].groupNames).toEqual(["Alpha Group", "Beta Group", "Zebra Group"]); + }); + + it("should sort collection names alphabetically", async () => { + const organization = createMockOrganization({ useGroups: false }); + const mockUser = createMockUserResponse({ + collections: [{ id: "col-1" } as any, { id: "col-2" } as any, { id: "col-3" } as any], + }); + const mockUsersResponse: ListResponse = { + data: [mockUser], + } as any; + const mockCollections = [ + createMockCollection("col-1", "Zebra Collection"), + createMockCollection("col-2", "Alpha Collection"), + createMockCollection("col-3", "Beta Collection"), + ]; + + organizationUserApiService.getAllUsers.mockResolvedValue(mockUsersResponse); + apiService.getCollections.mockResolvedValue({ + data: mockCollections, + } as any); + + const result = await service.loadUsers(organization); + + expect(result[0].collectionNames).toEqual([ + "Alpha Collection", + "Beta Collection", + "Zebra Collection", + ]); + }); + + it("should filter out null or undefined group names", async () => { + const organization = createMockOrganization({ useGroups: true }); + const mockUser = createMockUserResponse({ + groups: ["group-1", "group-2", "group-3"], + }); + const mockUsersResponse: ListResponse = { + data: [mockUser], + } as any; + const mockGroups = [ + createMockGroup("group-1", "Group 1"), + // group-2 is missing - should be filtered out + createMockGroup("group-3", "Group 3"), + ]; + + organizationUserApiService.getAllUsers.mockResolvedValue(mockUsersResponse); + groupService.getAll.mockResolvedValue(mockGroups as any); + + const result = await service.loadUsers(organization); + + expect(result[0].groupNames).toEqual(["Group 1", "Group 3"]); + expect(result[0].groupNames).not.toContain(undefined); + expect(result[0].groupNames).not.toContain(null); + }); + + it("should filter out null or undefined collection names", async () => { + const organization = createMockOrganization({ useGroups: false }); + const mockUser = createMockUserResponse({ + collections: [{ id: "col-1" } as any, { id: "col-2" } as any, { id: "col-3" } as any], + }); + const mockUsersResponse: ListResponse = { + data: [mockUser], + } as any; + const mockCollections = [ + createMockCollection("col-1", "Collection 1"), + // col-2 is missing - should be filtered out + createMockCollection("col-3", "Collection 3"), + ]; + + organizationUserApiService.getAllUsers.mockResolvedValue(mockUsersResponse); + apiService.getCollections.mockResolvedValue({ + data: mockCollections, + } as any); + + const result = await service.loadUsers(organization); + + expect(result[0].collectionNames).toEqual(["Collection 1", "Collection 3"]); + expect(result[0].collectionNames).not.toContain(undefined); + expect(result[0].collectionNames).not.toContain(null); + }); + + it("should handle multiple users", async () => { + const organization = createMockOrganization({ useGroups: true }); + const mockUser1 = createMockUserResponse({ + id: "user-1", + groups: ["group-1"], + }); + const mockUser2 = createMockUserResponse({ + id: "user-2", + groups: ["group-2"], + }); + const mockUsersResponse: ListResponse = { + data: [mockUser1, mockUser2], + } as any; + const mockGroups = [ + createMockGroup("group-1", "Group 1"), + createMockGroup("group-2", "Group 2"), + ]; + + organizationUserApiService.getAllUsers.mockResolvedValue(mockUsersResponse); + groupService.getAll.mockResolvedValue(mockGroups as any); + + const result = await service.loadUsers(organization); + + expect(result).toHaveLength(2); + expect(result[0].groupNames).toEqual(["Group 1"]); + expect(result[1].groupNames).toEqual(["Group 2"]); + }); + + it("should return empty array when usersResponse.data is null", async () => { + const organization = createMockOrganization({ useGroups: false }); + const mockUsersResponse: ListResponse = { + data: null as any, + } as any; + + organizationUserApiService.getAllUsers.mockResolvedValue(mockUsersResponse); + apiService.getCollections.mockResolvedValue({ + data: [], + } as any); + + const result = await service.loadUsers(organization); + + expect(result).toEqual([]); + }); + + it("should return empty array when usersResponse.data is undefined", async () => { + const organization = createMockOrganization({ useGroups: false }); + const mockUsersResponse: ListResponse = { + data: undefined as any, + } as any; + + organizationUserApiService.getAllUsers.mockResolvedValue(mockUsersResponse); + apiService.getCollections.mockResolvedValue({ + data: [], + } as any); + + const result = await service.loadUsers(organization); + + expect(result).toEqual([]); + }); + + it("should handle empty groups array", async () => { + const organization = createMockOrganization({ useGroups: true }); + const mockUser = createMockUserResponse({ + groups: [], + }); + const mockUsersResponse: ListResponse = { + data: [mockUser], + } as any; + + organizationUserApiService.getAllUsers.mockResolvedValue(mockUsersResponse); + groupService.getAll.mockResolvedValue([]); + + const result = await service.loadUsers(organization); + + expect(result).toHaveLength(1); + expect(result[0].groupNames).toEqual([]); + }); + + it("should handle empty collections array", async () => { + const organization = createMockOrganization({ useGroups: false }); + const mockUser = createMockUserResponse({ + collections: [], + }); + const mockUsersResponse: ListResponse = { + data: [mockUser], + } as any; + + organizationUserApiService.getAllUsers.mockResolvedValue(mockUsersResponse); + apiService.getCollections.mockResolvedValue({ + data: [], + } as any); + + const result = await service.loadUsers(organization); + + expect(result).toHaveLength(1); + expect(result[0].collectionNames).toEqual([]); + }); + + it("should fetch data in parallel using Promise.all", async () => { + const organization = createMockOrganization({ useGroups: true }); + const mockUsersResponse: ListResponse = { + data: [], + } as any; + + let getUsersCallTime: number; + let getGroupsCallTime: number; + + organizationUserApiService.getAllUsers.mockImplementation(async () => { + getUsersCallTime = Date.now(); + return mockUsersResponse; + }); + + groupService.getAll.mockImplementation(async () => { + getGroupsCallTime = Date.now(); + return []; + }); + + await service.loadUsers(organization); + + // Both calls should have been initiated at roughly the same time (within 50ms) + expect(Math.abs(getUsersCallTime - getGroupsCallTime)).toBeLessThan(50); + }); + }); +}); diff --git a/apps/web/src/app/admin-console/organizations/members/services/organization-members-service/organization-members.service.ts b/apps/web/src/app/admin-console/organizations/members/services/organization-members-service/organization-members.service.ts new file mode 100644 index 00000000000..613c7c1b9c0 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/services/organization-members-service/organization-members.service.ts @@ -0,0 +1,76 @@ +import { Injectable } from "@angular/core"; + +import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; + +import { GroupApiService } from "../../../core"; +import { OrganizationUserView } from "../../../core/views/organization-user.view"; + +@Injectable() +export class OrganizationMembersService { + constructor( + private organizationUserApiService: OrganizationUserApiService, + private groupService: GroupApiService, + private apiService: ApiService, + ) {} + + async loadUsers(organization: Organization): Promise { + let groupsPromise: Promise> | undefined; + let collectionsPromise: Promise> | undefined; + + const userPromise = this.organizationUserApiService.getAllUsers(organization.id, { + includeGroups: organization.useGroups, + includeCollections: !organization.useGroups, + }); + + if (organization.useGroups) { + groupsPromise = this.getGroupNameMap(organization); + } else { + collectionsPromise = this.getCollectionNameMap(organization); + } + + const [usersResponse, groupNamesMap, collectionNamesMap] = await Promise.all([ + userPromise, + groupsPromise, + collectionsPromise, + ]); + + return ( + usersResponse.data?.map((r) => { + const userView = OrganizationUserView.fromResponse(r); + + userView.groupNames = userView.groups + .map((g: string) => groupNamesMap?.get(g)) + .filter((name): name is string => name != null) + .sort(); + userView.collectionNames = userView.collections + .map((c: { id: string }) => collectionNamesMap?.get(c.id)) + .filter((name): name is string => name != null) + .sort(); + + return userView; + }) ?? [] + ); + } + + private async getGroupNameMap(organization: Organization): Promise> { + const groups = await this.groupService.getAll(organization.id); + const groupNameMap = new Map(); + groups.forEach((g: { id: string; name: string }) => groupNameMap.set(g.id, g.name)); + return groupNameMap; + } + + private async getCollectionNameMap(organization: Organization): Promise> { + const response = this.apiService + .getCollections(organization.id) + .then((res) => + res.data.map((r: { id: string; name: string }) => ({ id: r.id, name: r.name })), + ); + + const collections = await response; + const collectionMap = new Map(); + collections.forEach((c: { id: string; name: string }) => collectionMap.set(c.id, c.name)); + return collectionMap; + } +} diff --git a/apps/web/src/app/billing/members/billing-constraint/billing-constraint.service.spec.ts b/apps/web/src/app/billing/members/billing-constraint/billing-constraint.service.spec.ts new file mode 100644 index 00000000000..f7bb510f579 --- /dev/null +++ b/apps/web/src/app/billing/members/billing-constraint/billing-constraint.service.spec.ts @@ -0,0 +1,461 @@ +import { TestBed } from "@angular/core/testing"; +import { Router } from "@angular/router"; +import { of } from "rxjs"; + +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { OrganizationMetadataServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-metadata.service.abstraction"; +import { ProductTierType } from "@bitwarden/common/billing/enums"; +import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { OrganizationId } from "@bitwarden/common/types/guid"; +import { DialogService, ToastService } from "@bitwarden/components"; + +import { + ChangePlanDialogResultType, + openChangePlanDialog, +} from "../../organizations/change-plan-dialog.component"; + +import { BillingConstraintService, SeatLimitResult } from "./billing-constraint.service"; + +jest.mock("../../organizations/change-plan-dialog.component"); + +describe("BillingConstraintService", () => { + let service: BillingConstraintService; + let i18nService: jest.Mocked; + let dialogService: jest.Mocked; + let toastService: jest.Mocked; + let router: jest.Mocked; + let organizationMetadataService: jest.Mocked; + + const mockOrganizationId = "org-123" as OrganizationId; + + const createMockOrganization = (overrides: Partial = {}): Organization => { + const org = new Organization(); + org.id = mockOrganizationId; + org.seats = 10; + org.productTierType = ProductTierType.Teams; + + Object.defineProperty(org, "hasReseller", { + value: false, + writable: true, + configurable: true, + }); + + Object.defineProperty(org, "canEditSubscription", { + value: true, + writable: true, + configurable: true, + }); + + return Object.assign(org, overrides); + }; + + const createMockBillingMetadata = ( + overrides: Partial = {}, + ): OrganizationBillingMetadataResponse => { + return { + organizationOccupiedSeats: 5, + ...overrides, + } as OrganizationBillingMetadataResponse; + }; + + beforeEach(() => { + const mockDialogRef = { + closed: of(true), + }; + + const mockSimpleDialogRef = { + closed: of(true), + }; + + i18nService = { + t: jest.fn().mockReturnValue("translated-text"), + } as any; + + dialogService = { + openSimpleDialogRef: jest.fn().mockReturnValue(mockSimpleDialogRef), + } as any; + + toastService = { + showToast: jest.fn(), + } as any; + + router = { + navigate: jest.fn().mockResolvedValue(true), + } as any; + + organizationMetadataService = { + getOrganizationMetadata$: jest.fn(), + refreshMetadataCache: jest.fn(), + } as any; + + (openChangePlanDialog as jest.Mock).mockReturnValue(mockDialogRef); + + TestBed.configureTestingModule({ + providers: [ + BillingConstraintService, + { provide: I18nService, useValue: i18nService }, + { provide: DialogService, useValue: dialogService }, + { provide: ToastService, useValue: toastService }, + { provide: Router, useValue: router }, + { provide: OrganizationMetadataServiceAbstraction, useValue: organizationMetadataService }, + ], + }); + + service = TestBed.inject(BillingConstraintService); + }); + + describe("checkSeatLimit", () => { + it("should allow users when occupied seats are less than total seats", () => { + const organization = createMockOrganization({ seats: 10 }); + const billingMetadata = createMockBillingMetadata({ organizationOccupiedSeats: 5 }); + + const result = service.checkSeatLimit(organization, billingMetadata); + + expect(result).toEqual({ canAddUsers: true }); + }); + + it("should allow users when occupied seats equal total seats for non-fixed seat plans", () => { + const organization = createMockOrganization({ + seats: 10, + productTierType: ProductTierType.Teams, + }); + const billingMetadata = createMockBillingMetadata({ organizationOccupiedSeats: 10 }); + + const result = service.checkSeatLimit(organization, billingMetadata); + + expect(result).toEqual({ canAddUsers: true }); + }); + + it("should block users with reseller-limit reason when organization has reseller", () => { + const organization = createMockOrganization({ + seats: 10, + hasReseller: true, + }); + const billingMetadata = createMockBillingMetadata({ organizationOccupiedSeats: 10 }); + + const result = service.checkSeatLimit(organization, billingMetadata); + + expect(result).toEqual({ + canAddUsers: false, + reason: "reseller-limit", + }); + }); + + it("should block users with fixed-seat-limit reason for fixed seat plans", () => { + const organization = createMockOrganization({ + seats: 10, + productTierType: ProductTierType.Free, + canEditSubscription: true, + }); + const billingMetadata = createMockBillingMetadata({ organizationOccupiedSeats: 10 }); + + const result = service.checkSeatLimit(organization, billingMetadata); + + expect(result).toEqual({ + canAddUsers: false, + reason: "fixed-seat-limit", + shouldShowUpgradeDialog: true, + }); + }); + + it("should not show upgrade dialog when organization cannot edit subscription", () => { + const organization = createMockOrganization({ + seats: 10, + productTierType: ProductTierType.TeamsStarter, + canEditSubscription: false, + }); + const billingMetadata = createMockBillingMetadata({ organizationOccupiedSeats: 10 }); + + const result = service.checkSeatLimit(organization, billingMetadata); + + expect(result).toEqual({ + canAddUsers: false, + reason: "fixed-seat-limit", + shouldShowUpgradeDialog: false, + }); + }); + + it("shoud throw if missing billingMetadata", () => { + const organization = createMockOrganization({ seats: 10 }); + const billingMetadata = createMockBillingMetadata({ + organizationOccupiedSeats: undefined as any, + }); + + const err = () => service.checkSeatLimit(organization, billingMetadata); + + expect(err).toThrow("Cannot check seat limit: billingMetadata is null or undefined."); + }); + }); + + describe("seatLimitReached", () => { + it("should return false when canAddUsers is true", async () => { + const result: SeatLimitResult = { canAddUsers: true }; + const organization = createMockOrganization(); + + const seatLimitReached = await service.seatLimitReached(result, organization); + + expect(seatLimitReached).toBe(false); + }); + + it("should show toast and return true for reseller-limit", async () => { + const result: SeatLimitResult = { canAddUsers: false, reason: "reseller-limit" }; + const organization = createMockOrganization(); + + const seatLimitReached = await service.seatLimitReached(result, organization); + + expect(toastService.showToast).toHaveBeenCalledWith({ + variant: "error", + title: "translated-text", + message: "translated-text", + }); + expect(i18nService.t).toHaveBeenCalledWith("seatLimitReached"); + expect(i18nService.t).toHaveBeenCalledWith("contactYourProvider"); + expect(seatLimitReached).toBe(true); + }); + + it("should return true when upgrade dialog is cancelled", async () => { + const result: SeatLimitResult = { + canAddUsers: false, + reason: "fixed-seat-limit", + shouldShowUpgradeDialog: true, + }; + const organization = createMockOrganization(); + const mockDialogRef = { closed: of(ChangePlanDialogResultType.Closed) }; + (openChangePlanDialog as jest.Mock).mockReturnValue(mockDialogRef); + + const seatLimitReached = await service.seatLimitReached(result, organization); + + expect(openChangePlanDialog).toHaveBeenCalledWith(dialogService, { + data: { + organizationId: organization.id, + productTierType: organization.productTierType, + }, + }); + expect(seatLimitReached).toBe(true); + }); + + it("should return false when upgrade dialog is submitted", async () => { + const result: SeatLimitResult = { + canAddUsers: false, + reason: "fixed-seat-limit", + shouldShowUpgradeDialog: true, + }; + const organization = createMockOrganization(); + const mockDialogRef = { closed: of(ChangePlanDialogResultType.Submitted) }; + (openChangePlanDialog as jest.Mock).mockReturnValue(mockDialogRef); + + const seatLimitReached = await service.seatLimitReached(result, organization); + + expect(seatLimitReached).toBe(false); + }); + + it("should show seat limit dialog when shouldShowUpgradeDialog is false", async () => { + const result: SeatLimitResult = { + canAddUsers: false, + reason: "fixed-seat-limit", + shouldShowUpgradeDialog: false, + }; + const organization = createMockOrganization({ + canEditSubscription: false, + productTierType: ProductTierType.Free, + }); + + const seatLimitReached = await service.seatLimitReached(result, organization); + + expect(dialogService.openSimpleDialogRef).toHaveBeenCalled(); + expect(seatLimitReached).toBe(true); + }); + + it("should return true for unknown reasons", async () => { + const result: SeatLimitResult = { canAddUsers: false }; + const organization = createMockOrganization(); + + const seatLimitReached = await service.seatLimitReached(result, organization); + + expect(seatLimitReached).toBe(true); + }); + }); + + describe("navigateToPaymentMethod", () => { + it("should navigate to payment method with correct parameters", async () => { + const organization = createMockOrganization(); + + await service.navigateToPaymentMethod(organization); + + expect(router.navigate).toHaveBeenCalledWith( + ["organizations", organization.id, "billing", "payment-method"], + { + state: { launchPaymentModalAutomatically: true }, + }, + ); + }); + }); + + describe("private methods through public method coverage", () => { + describe("getDialogContent via showSeatLimitReachedDialog", () => { + it("should get correct dialog content for Free organization", async () => { + const result: SeatLimitResult = { + canAddUsers: false, + reason: "fixed-seat-limit", + shouldShowUpgradeDialog: false, + }; + const organization = createMockOrganization({ + productTierType: ProductTierType.Free, + canEditSubscription: false, + seats: 5, + }); + + await service.seatLimitReached(result, organization); + + expect(i18nService.t).toHaveBeenCalledWith("freeOrgInvLimitReachedNoManageBilling", 5); + }); + + it("should get correct dialog content for TeamsStarter organization", async () => { + const result: SeatLimitResult = { + canAddUsers: false, + reason: "fixed-seat-limit", + shouldShowUpgradeDialog: false, + }; + const organization = createMockOrganization({ + productTierType: ProductTierType.TeamsStarter, + canEditSubscription: false, + seats: 3, + }); + + await service.seatLimitReached(result, organization); + + expect(i18nService.t).toHaveBeenCalledWith( + "teamsStarterPlanInvLimitReachedNoManageBilling", + 3, + ); + }); + + it("should get correct dialog content for Families organization", async () => { + const result: SeatLimitResult = { + canAddUsers: false, + reason: "fixed-seat-limit", + shouldShowUpgradeDialog: false, + }; + const organization = createMockOrganization({ + productTierType: ProductTierType.Families, + canEditSubscription: false, + seats: 6, + }); + + await service.seatLimitReached(result, organization); + + expect(i18nService.t).toHaveBeenCalledWith("familiesPlanInvLimitReachedNoManageBilling", 6); + }); + + it("should throw error for unsupported product type in getProductKey", async () => { + const result: SeatLimitResult = { + canAddUsers: false, + reason: "fixed-seat-limit", + shouldShowUpgradeDialog: false, + }; + const organization = createMockOrganization({ + productTierType: ProductTierType.Enterprise, + canEditSubscription: false, + }); + + await expect(service.seatLimitReached(result, organization)).rejects.toThrow( + `Unsupported product type: ${ProductTierType.Enterprise}`, + ); + }); + }); + + describe("getAcceptButtonText via showSeatLimitReachedDialog", () => { + it("should return 'ok' when organization cannot edit subscription", async () => { + const result: SeatLimitResult = { + canAddUsers: false, + reason: "fixed-seat-limit", + shouldShowUpgradeDialog: false, + }; + const organization = createMockOrganization({ + canEditSubscription: false, + productTierType: ProductTierType.Free, + }); + + await service.seatLimitReached(result, organization); + + expect(i18nService.t).toHaveBeenCalledWith("ok"); + }); + + it("should return 'upgrade' when organization can edit subscription", async () => { + const result: SeatLimitResult = { + canAddUsers: false, + reason: "fixed-seat-limit", + shouldShowUpgradeDialog: false, + }; + const organization = createMockOrganization({ + canEditSubscription: true, + productTierType: ProductTierType.Free, + }); + const mockSimpleDialogRef = { closed: of(false) }; + dialogService.openSimpleDialogRef.mockReturnValue(mockSimpleDialogRef); + + await service.seatLimitReached(result, organization); + + expect(i18nService.t).toHaveBeenCalledWith("upgrade"); + }); + + it("should throw error for unsupported product type in getAcceptButtonText", async () => { + const result: SeatLimitResult = { + canAddUsers: false, + reason: "fixed-seat-limit", + shouldShowUpgradeDialog: false, + }; + const organization = createMockOrganization({ + canEditSubscription: true, + productTierType: ProductTierType.Enterprise, + }); + + await expect(service.seatLimitReached(result, organization)).rejects.toThrow( + `Unsupported product type: ${ProductTierType.Enterprise}`, + ); + }); + }); + + describe("handleUpgradeNavigation", () => { + it("should navigate to billing subscription with upgrade query param", async () => { + const result: SeatLimitResult = { + canAddUsers: false, + reason: "fixed-seat-limit", + shouldShowUpgradeDialog: false, + }; + const organization = createMockOrganization({ + canEditSubscription: true, + productTierType: ProductTierType.Free, + }); + const mockSimpleDialogRef = { closed: of(true) }; + dialogService.openSimpleDialogRef.mockReturnValue(mockSimpleDialogRef); + + await service.seatLimitReached(result, organization); + + expect(router.navigate).toHaveBeenCalledWith( + ["/organizations", organization.id, "billing", "subscription"], + { queryParams: { upgrade: true } }, + ); + }); + + it("should throw error for non-self-upgradable product type", async () => { + const result: SeatLimitResult = { + canAddUsers: false, + reason: "fixed-seat-limit", + shouldShowUpgradeDialog: false, + }; + const organization = createMockOrganization({ + canEditSubscription: true, + productTierType: ProductTierType.Enterprise, + }); + const mockSimpleDialogRef = { closed: of(true) }; + dialogService.openSimpleDialogRef.mockReturnValue(mockSimpleDialogRef); + + await expect(service.seatLimitReached(result, organization)).rejects.toThrow( + `Unsupported product type: ${ProductTierType.Enterprise}`, + ); + }); + }); + }); +}); diff --git a/apps/web/src/app/billing/members/billing-constraint/billing-constraint.service.ts b/apps/web/src/app/billing/members/billing-constraint/billing-constraint.service.ts new file mode 100644 index 00000000000..d43c2e68497 --- /dev/null +++ b/apps/web/src/app/billing/members/billing-constraint/billing-constraint.service.ts @@ -0,0 +1,192 @@ +import { Injectable } from "@angular/core"; +import { Router } from "@angular/router"; +import { lastValueFrom } from "rxjs"; + +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { isNotSelfUpgradable, ProductTierType } from "@bitwarden/common/billing/enums"; +import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogService, ToastService } from "@bitwarden/components"; + +import { isFixedSeatPlan } from "../../../admin-console/organizations/members/components/member-dialog/validators/org-seat-limit-reached.validator"; +import { + ChangePlanDialogResultType, + openChangePlanDialog, +} from "../../organizations/change-plan-dialog.component"; + +export interface SeatLimitResult { + canAddUsers: boolean; + reason?: "reseller-limit" | "fixed-seat-limit" | "no-billing-permission"; + shouldShowUpgradeDialog?: boolean; +} + +@Injectable() +export class BillingConstraintService { + constructor( + private i18nService: I18nService, + private dialogService: DialogService, + private toastService: ToastService, + private router: Router, + ) {} + + checkSeatLimit( + organization: Organization, + billingMetadata: OrganizationBillingMetadataResponse, + ): SeatLimitResult { + const occupiedSeats = billingMetadata?.organizationOccupiedSeats; + if (occupiedSeats == null) { + throw new Error("Cannot check seat limit: billingMetadata is null or undefined."); + } + const totalSeats = organization.seats; + + if (occupiedSeats < totalSeats) { + return { canAddUsers: true }; + } + + if (organization.hasReseller) { + return { + canAddUsers: false, + reason: "reseller-limit", + }; + } + + if (isFixedSeatPlan(organization.productTierType)) { + return { + canAddUsers: false, + reason: "fixed-seat-limit", + shouldShowUpgradeDialog: organization.canEditSubscription, + }; + } + + return { canAddUsers: true }; + } + + async seatLimitReached(result: SeatLimitResult, organization: Organization): Promise { + if (result.canAddUsers) { + return false; + } + + switch (result.reason) { + case "reseller-limit": + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("seatLimitReached"), + message: this.i18nService.t("contactYourProvider"), + }); + return true; + + case "fixed-seat-limit": + if (result.shouldShowUpgradeDialog) { + const dialogResult = await this.showChangePlanDialog(organization); + // If the plan was successfully changed, the seat limit is no longer blocking + return dialogResult !== ChangePlanDialogResultType.Submitted; + } else { + await this.showSeatLimitReachedDialog(organization); + return true; + } + + default: + return true; + } + } + + private async showChangePlanDialog( + organization: Organization, + ): Promise { + const reference = openChangePlanDialog(this.dialogService, { + data: { + organizationId: organization.id, + productTierType: organization.productTierType, + }, + }); + + const result = await lastValueFrom(reference.closed); + if (result == null) { + throw new Error("ChangePlanDialog result is null or undefined."); + } + + return result; + } + + private async showSeatLimitReachedDialog(organization: Organization): Promise { + const dialogContent = this.getSeatLimitReachedDialogContent(organization); + const acceptButtonText = this.getSeatLimitReachedDialogAcceptButtonText(organization); + + const orgUpgradeSimpleDialogOpts = { + title: this.i18nService.t("upgradeOrganization"), + content: dialogContent, + type: "primary" as const, + acceptButtonText, + cancelButtonText: organization.canEditSubscription ? undefined : (null as string | null), + }; + + const simpleDialog = this.dialogService.openSimpleDialogRef(orgUpgradeSimpleDialogOpts); + const result = await lastValueFrom(simpleDialog.closed); + + if (result && organization.canEditSubscription) { + await this.handleUpgradeNavigation(organization); + } + } + + private async handleUpgradeNavigation(organization: Organization): Promise { + const productType = organization.productTierType; + + if (isNotSelfUpgradable(productType)) { + throw new Error(`Unsupported product type: ${organization.productTierType}`); + } + + await this.router.navigate(["/organizations", organization.id, "billing", "subscription"], { + queryParams: { upgrade: true }, + }); + } + + private getSeatLimitReachedDialogContent(organization: Organization): string { + const productKey = this.getProductKey(organization); + return this.i18nService.t(productKey, organization.seats); + } + + private getSeatLimitReachedDialogAcceptButtonText(organization: Organization): string { + if (!organization.canEditSubscription) { + return this.i18nService.t("ok"); + } + + const productType = organization.productTierType; + + if (isNotSelfUpgradable(productType)) { + throw new Error(`Unsupported product type: ${productType}`); + } + + return this.i18nService.t("upgrade"); + } + + private getProductKey(organization: Organization): string { + const manageBillingText = organization.canEditSubscription + ? "ManageBilling" + : "NoManageBilling"; + + let product = ""; + switch (organization.productTierType) { + case ProductTierType.Free: + product = "freeOrg"; + break; + case ProductTierType.TeamsStarter: + product = "teamsStarterPlan"; + break; + case ProductTierType.Families: + product = "familiesPlan"; + break; + default: + throw new Error(`Unsupported product type: ${organization.productTierType}`); + } + return `${product}InvLimitReached${manageBillingText}`; + } + + async navigateToPaymentMethod(organization: Organization): Promise { + await this.router.navigate( + ["organizations", `${organization.id}`, "billing", "payment-method"], + { + state: { launchPaymentModalAutomatically: true }, + }, + ); + } +} 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 affcfce9c17..e86956dec93 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 @@ -30,6 +30,7 @@ import { } from "@bitwarden/web-vault/app/admin-console/common/people-table-data-source"; import { openEntityEventsDialog } from "@bitwarden/web-vault/app/admin-console/organizations/manage/entity-events.component"; import { BulkStatusComponent } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/bulk-status.component"; +import { MemberActionResult } from "@bitwarden/web-vault/app/admin-console/organizations/members/services/member-actions/member-actions.service"; import { AddEditMemberDialogComponent, @@ -199,16 +200,27 @@ export class MembersComponent extends BaseMembersComponent { await this.load(); } - async confirmUser(user: ProviderUser, publicKey: Uint8Array): Promise { - const providerKey = await this.keyService.getProviderKey(this.providerId); - 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); + async confirmUser(user: ProviderUser, publicKey: Uint8Array): Promise { + try { + const providerKey = await this.keyService.getProviderKey(this.providerId); + 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); + return { success: true }; + } catch (error) { + return { success: false, error: error.message }; + } } - removeUser = (id: string): Promise => - this.apiService.deleteProviderUser(this.providerId, id); + removeUser = async (id: string): Promise => { + try { + await this.apiService.deleteProviderUser(this.providerId, id); + return { success: true }; + } catch (error) { + return { success: false, error: error.message }; + } + }; edit = async (user: ProviderUser | null): Promise => { const data: AddEditMemberDialogParams = { @@ -251,6 +263,12 @@ export class MembersComponent extends BaseMembersComponent { getUsers = (): Promise> => this.apiService.getProviderUsers(this.providerId); - reinviteUser = (id: string): Promise => - this.apiService.postProviderUserReinvite(this.providerId, id); + reinviteUser = async (id: string): Promise => { + try { + await this.apiService.postProviderUserReinvite(this.providerId, id); + return { success: true }; + } catch (error) { + return { success: false, error: error.message }; + } + }; } diff --git a/libs/common/src/billing/services/organization/organization-metadata.service.spec.ts b/libs/common/src/billing/services/organization/organization-metadata.service.spec.ts index 0ed60bef605..c67f4aed175 100644 --- a/libs/common/src/billing/services/organization/organization-metadata.service.spec.ts +++ b/libs/common/src/billing/services/organization/organization-metadata.service.spec.ts @@ -208,7 +208,7 @@ describe("DefaultOrganizationMetadataService", () => { }, 10); }); - it("does not trigger refresh when feature flag is disabled", async () => { + it("does trigger refresh when feature flag is disabled", async () => { featureFlagSubject.next(false); const mockResponse1 = createMockMetadataResponse(false, 10); @@ -232,11 +232,10 @@ describe("DefaultOrganizationMetadataService", () => { service.refreshMetadataCache(); - // wait to ensure no additional invocations await new Promise((resolve) => setTimeout(resolve, 10)); - expect(invocationCount).toBe(1); - expect(billingApiService.getOrganizationBillingMetadata).toHaveBeenCalledTimes(1); + expect(invocationCount).toBe(2); + expect(billingApiService.getOrganizationBillingMetadata).toHaveBeenCalledTimes(2); subscription.unsubscribe(); }); diff --git a/libs/common/src/billing/services/organization/organization-metadata.service.ts b/libs/common/src/billing/services/organization/organization-metadata.service.ts index 09aaa202112..fe96f0d984c 100644 --- a/libs/common/src/billing/services/organization/organization-metadata.service.ts +++ b/libs/common/src/billing/services/organization/organization-metadata.service.ts @@ -1,4 +1,4 @@ -import { filter, from, merge, Observable, shareReplay, Subject, switchMap } from "rxjs"; +import { BehaviorSubject, combineLatest, from, Observable, shareReplay, switchMap } from "rxjs"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; @@ -18,57 +18,56 @@ export class DefaultOrganizationMetadataService implements OrganizationMetadataS private billingApiService: BillingApiServiceAbstraction, private configService: ConfigService, ) {} - private refreshMetadataTrigger = new Subject(); + private refreshMetadataTrigger = new BehaviorSubject(undefined); - refreshMetadataCache = () => this.refreshMetadataTrigger.next(); + refreshMetadataCache = () => { + this.metadataCache.clear(); + this.refreshMetadataTrigger.next(); + }; - getOrganizationMetadata$ = ( - organizationId: OrganizationId, - ): Observable => - this.configService - .getFeatureFlag$(FeatureFlag.PM25379_UseNewOrganizationMetadataStructure) - .pipe( - switchMap((featureFlagEnabled) => { - return merge( - this.getOrganizationMetadataInternal$(organizationId, featureFlagEnabled), - this.refreshMetadataTrigger.pipe( - filter(() => featureFlagEnabled), - switchMap(() => - this.getOrganizationMetadataInternal$(organizationId, featureFlagEnabled, true), - ), - ), - ); - }), - ); + getOrganizationMetadata$(orgId: OrganizationId): Observable { + return combineLatest([ + this.refreshMetadataTrigger, + this.configService.getFeatureFlag$(FeatureFlag.PM25379_UseNewOrganizationMetadataStructure), + ]).pipe( + switchMap(([_, featureFlagEnabled]) => + featureFlagEnabled + ? this.vNextGetOrganizationMetadataInternal$(orgId) + : this.getOrganizationMetadataInternal$(orgId), + ), + ); + } - private getOrganizationMetadataInternal$( - organizationId: OrganizationId, - featureFlagEnabled: boolean, - bypassCache: boolean = false, + private vNextGetOrganizationMetadataInternal$( + orgId: OrganizationId, ): Observable { - if (!bypassCache && featureFlagEnabled && this.metadataCache.has(organizationId)) { - return this.metadataCache.get(organizationId)!; + const cacheHit = this.metadataCache.get(orgId); + if (cacheHit) { + return cacheHit; } - const metadata$ = from(this.fetchMetadata(organizationId, featureFlagEnabled)).pipe( + const result = from(this.fetchMetadata(orgId, true)).pipe( shareReplay({ bufferSize: 1, refCount: false }), ); - if (featureFlagEnabled) { - this.metadataCache.set(organizationId, metadata$); - } + this.metadataCache.set(orgId, result); + return result; + } - return metadata$; + private getOrganizationMetadataInternal$( + organizationId: OrganizationId, + ): Observable { + return from(this.fetchMetadata(organizationId, false)).pipe( + shareReplay({ bufferSize: 1, refCount: false }), + ); } private async fetchMetadata( organizationId: OrganizationId, featureFlagEnabled: boolean, ): Promise { - if (featureFlagEnabled) { - return await this.billingApiService.getOrganizationBillingMetadataVNext(organizationId); - } - - return await this.billingApiService.getOrganizationBillingMetadata(organizationId); + return featureFlagEnabled + ? await this.billingApiService.getOrganizationBillingMetadataVNext(organizationId) + : await this.billingApiService.getOrganizationBillingMetadata(organizationId); } } From 3790e09673aa37fde19d7097514a6d323cbe5f53 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Thu, 23 Oct 2025 17:25:48 +0200 Subject: [PATCH 09/73] AC - Prefer signal & change detection (#16948) * Modernize Angular * Remove conflicted files --- .../bulk-collections-dialog.component.ts | 2 + .../collection-access-restricted.component.ts | 8 ++++ .../collection-name.badge.component.ts | 6 +++ .../group-badge/group-name-badge.component.ts | 6 +++ .../vault-filter/vault-filter.component.ts | 4 ++ .../vault-header/vault-header.component.ts | 22 ++++++++++ .../collections/vault.component.ts | 4 ++ .../organization-information.component.ts | 14 ++++++ .../guards/is-enterprise-org.guard.spec.ts | 6 +++ .../guards/is-paid-org.guard.spec.ts | 6 +++ .../guards/org-redirect.guard.spec.ts | 6 +++ .../layouts/organization-layout.component.ts | 2 + .../manage/entity-events.component.ts | 2 + .../organizations/manage/events.component.ts | 2 + .../manage/group-add-edit.component.ts | 2 + .../organizations/manage/groups.component.ts | 2 + .../manage/user-confirm.component.ts | 2 + .../verify-recover-delete-org.component.ts | 2 + .../account-recovery-dialog.component.ts | 4 ++ .../bulk/bulk-confirm-dialog.component.ts | 2 + .../bulk/bulk-delete-dialog.component.ts | 2 + .../bulk/bulk-enable-sm-dialog.component.ts | 2 + .../bulk/bulk-remove-dialog.component.ts | 2 + .../bulk/bulk-restore-revoke.component.ts | 2 + .../components/bulk/bulk-status.component.ts | 2 + .../member-dialog/member-dialog.component.ts | 2 + .../nested-checkbox.component.ts | 6 +++ .../members/members.component.ts | 2 + .../policies/base-policy-edit.component.ts | 4 ++ .../policies/policies.component.ts | 2 + .../autotype-policy.component.ts | 2 + .../disable-send.component.ts | 2 + .../master-password.component.ts | 2 + .../organization-data-ownership.component.ts | 2 + .../password-generator.component.ts | 2 + .../remove-unlock-with-pin.component.ts | 2 + .../require-sso.component.ts | 2 + .../reset-password.component.ts | 2 + .../restricted-item-types.component.ts | 2 + .../send-options.component.ts | 2 + .../single-org.component.ts | 2 + .../two-factor-authentication.component.ts | 2 + ...t-organization-data-ownership.component.ts | 4 ++ .../policies/policy-edit-dialog.component.ts | 4 ++ .../reporting/reports-home.component.ts | 2 + .../settings/account.component.ts | 2 + .../delete-organization-dialog.component.ts | 2 + .../settings/two-factor-setup.component.ts | 2 + .../access-selector.component.ts | 43 ++++++++++++++++--- .../collection-dialog.component.ts | 2 + .../accept-family-sponsorship.component.ts | 2 + ...families-for-enterprise-setup.component.ts | 4 ++ .../settings/create-organization.component.ts | 2 + .../device-approvals.component.ts | 2 + .../domain-add-edit-dialog.component.ts | 2 + .../domain-verification.component.ts | 2 + .../organizations/manage/scim.component.ts | 2 + .../activate-autofill.component.ts | 2 + .../automatic-app-login.component.ts | 2 + ...disable-personal-vault-export.component.ts | 2 + .../maximum-vault-timeout.component.ts | 2 + .../manage/accept-provider.component.ts | 2 + .../add-edit-member-dialog.component.ts | 2 + .../dialogs/bulk-confirm-dialog.component.ts | 2 + .../dialogs/bulk-remove-dialog.component.ts | 2 + .../providers/manage/events.component.ts | 2 + .../providers/manage/members.component.ts | 2 + .../providers/providers-layout.component.ts | 2 + .../providers/providers.component.ts | 2 + .../providers/settings/account.component.ts | 2 + .../setup/setup-provider.component.ts | 2 + .../providers/setup/setup.component.ts | 4 ++ ...erify-recover-delete-provider.component.ts | 2 + 73 files changed, 258 insertions(+), 7 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/collections/bulk-collections-dialog/bulk-collections-dialog.component.ts b/apps/web/src/app/admin-console/organizations/collections/bulk-collections-dialog/bulk-collections-dialog.component.ts index 7c4e2156ffb..b8c82ac2f01 100644 --- a/apps/web/src/app/admin-console/organizations/collections/bulk-collections-dialog/bulk-collections-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/bulk-collections-dialog/bulk-collections-dialog.component.ts @@ -50,6 +50,8 @@ export enum BulkCollectionsDialogResult { Canceled = "canceled", } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ imports: [SharedModule, AccessSelectorModule], selector: "app-bulk-collections-dialog", diff --git a/apps/web/src/app/admin-console/organizations/collections/collection-access-restricted.component.ts b/apps/web/src/app/admin-console/organizations/collections/collection-access-restricted.component.ts index 86b83d75ca4..eafa3f4470a 100644 --- a/apps/web/src/app/admin-console/organizations/collections/collection-access-restricted.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/collection-access-restricted.component.ts @@ -6,6 +6,8 @@ import { ButtonModule, NoItemsModule } from "@bitwarden/components"; import { SharedModule } from "../../../shared"; import { CollectionDialogTabType } from "../shared/components/collection-dialog"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "collection-access-restricted", imports: [SharedModule, ButtonModule, NoItemsModule], @@ -37,9 +39,15 @@ export class CollectionAccessRestrictedComponent { protected icon = RestrictedView; protected collectionDialogTabType = CollectionDialogTabType; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() canEditCollection = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() canViewCollectionInfo = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() viewCollectionClicked = new EventEmitter<{ readonly: boolean; tab: CollectionDialogTabType; diff --git a/apps/web/src/app/admin-console/organizations/collections/collection-badge/collection-name.badge.component.ts b/apps/web/src/app/admin-console/organizations/collections/collection-badge/collection-name.badge.component.ts index d3893b5bd24..70a2e40001a 100644 --- a/apps/web/src/app/admin-console/organizations/collections/collection-badge/collection-name.badge.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/collection-badge/collection-name.badge.component.ts @@ -9,13 +9,19 @@ import { CollectionId } from "@bitwarden/sdk-internal"; import { SharedModule } from "../../../../shared/shared.module"; import { GetCollectionNameFromIdPipe } from "../pipes"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-collection-badge", templateUrl: "collection-name-badge.component.html", imports: [SharedModule, GetCollectionNameFromIdPipe], }) export class CollectionNameBadgeComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() collectionIds: CollectionId[] | string[]; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() collections: CollectionView[]; get shownCollections(): string[] { diff --git a/apps/web/src/app/admin-console/organizations/collections/group-badge/group-name-badge.component.ts b/apps/web/src/app/admin-console/organizations/collections/group-badge/group-name-badge.component.ts index 8f703acf9af..8a58f5b92d7 100644 --- a/apps/web/src/app/admin-console/organizations/collections/group-badge/group-name-badge.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/group-badge/group-name-badge.component.ts @@ -7,13 +7,19 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { GroupView } from "../../core"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-group-badge", templateUrl: "group-name-badge.component.html", standalone: false, }) export class GroupNameBadgeComponent implements OnChanges { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() selectedGroups: SelectionReadOnlyRequest[]; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() allGroups: GroupView[]; protected groupNames: string[] = []; diff --git a/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts index 3341a428970..01e61f0ab28 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts @@ -24,6 +24,8 @@ import { } from "../../../../vault/individual-vault/vault-filter/shared/models/vault-filter-section.type"; import { CollectionFilter } from "../../../../vault/individual-vault/vault-filter/shared/models/vault-filter.type"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-organization-vault-filter", templateUrl: @@ -34,6 +36,8 @@ export class VaultFilterComponent extends BaseVaultFilterComponent implements OnInit, OnDestroy, OnChanges { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() set organization(value: Organization) { if (value && value !== this._organization) { this._organization = value; diff --git a/apps/web/src/app/admin-console/organizations/collections/vault-header/vault-header.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault-header/vault-header.component.ts index 1be16c65cb8..30582063ab2 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault-header/vault-header.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault-header/vault-header.component.ts @@ -37,6 +37,8 @@ import { } from "../../../../vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model"; import { CollectionDialogTabType } from "../../shared/components/collection-dialog"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-org-vault-header", templateUrl: "./vault-header.component.html", @@ -59,36 +61,56 @@ export class VaultHeaderComponent { * Boolean to determine the loading state of the header. * Shows a loading spinner if set to true */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() loading: boolean; /** Current active filter */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() filter: RoutedVaultFilterModel; /** The organization currently being viewed */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() organization: Organization; /** Currently selected collection */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() collection?: TreeNode; /** The current search text in the header */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() searchText: string; /** Emits an event when the new item button is clicked in the header */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onAddCipher = new EventEmitter(); /** Emits an event when the new collection button is clicked in the header */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onAddCollection = new EventEmitter(); /** Emits an event when the edit collection button is clicked in the header */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onEditCollection = new EventEmitter<{ tab: CollectionDialogTabType; readonly: boolean; }>(); /** Emits an event when the delete collection button is clicked in the header */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onDeleteCollection = new EventEmitter(); /** Emits an event when the search text changes in the header*/ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() searchTextChanged = new EventEmitter(); protected CollectionDialogTabType = CollectionDialogTabType; diff --git a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts index b961de9e24c..eb4e47e0ffd 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts @@ -140,6 +140,8 @@ enum AddAccessStatusType { AddAccess = 1, } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-org-vault", templateUrl: "vault.component.html", @@ -207,6 +209,8 @@ export class VaultComponent implements OnInit, OnDestroy { protected selectedCollection$: Observable | undefined>; private nestedCollections$: Observable[]>; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("vaultItems", { static: false }) vaultItemsComponent: | VaultItemsComponent | undefined; diff --git a/apps/web/src/app/admin-console/organizations/create/organization-information.component.ts b/apps/web/src/app/admin-console/organizations/create/organization-information.component.ts index cd14b73a156..d45e06ad239 100644 --- a/apps/web/src/app/admin-console/organizations/create/organization-information.component.ts +++ b/apps/web/src/app/admin-console/organizations/create/organization-information.component.ts @@ -6,17 +6,31 @@ import { firstValueFrom } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-org-info", templateUrl: "organization-information.component.html", standalone: false, }) export class OrganizationInformationComponent implements OnInit { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() nameOnly = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() createOrganization = true; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() isProvider = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() acceptingSponsorship = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() formGroup: UntypedFormGroup; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() changedBusinessOwned = new EventEmitter(); constructor(private accountService: AccountService) {} diff --git a/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.spec.ts b/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.spec.ts index bc4a942301a..b463d24ea3c 100644 --- a/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.spec.ts +++ b/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.spec.ts @@ -19,18 +19,24 @@ import { DialogService } from "@bitwarden/components"; import { isEnterpriseOrgGuard } from "./is-enterprise-org.guard"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: "

This is the home screen!

", standalone: false, }) export class HomescreenComponent {} +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: "

This component can only be accessed by a enterprise organization!

", standalone: false, }) export class IsEnterpriseOrganizationComponent {} +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: "

This is the organization upgrade screen!

", standalone: false, diff --git a/apps/web/src/app/admin-console/organizations/guards/is-paid-org.guard.spec.ts b/apps/web/src/app/admin-console/organizations/guards/is-paid-org.guard.spec.ts index ab5fd79321a..d7c4e247d8e 100644 --- a/apps/web/src/app/admin-console/organizations/guards/is-paid-org.guard.spec.ts +++ b/apps/web/src/app/admin-console/organizations/guards/is-paid-org.guard.spec.ts @@ -18,18 +18,24 @@ import { DialogService } from "@bitwarden/components"; import { isPaidOrgGuard } from "./is-paid-org.guard"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: "

This is the home screen!

", standalone: false, }) export class HomescreenComponent {} +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: "

This component can only be accessed by a paid organization!

", standalone: false, }) export class PaidOrganizationOnlyComponent {} +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: "

This is the organization upgrade screen!

", standalone: false, diff --git a/apps/web/src/app/admin-console/organizations/guards/org-redirect.guard.spec.ts b/apps/web/src/app/admin-console/organizations/guards/org-redirect.guard.spec.ts index 9dc084484f3..38f13c4d781 100644 --- a/apps/web/src/app/admin-console/organizations/guards/org-redirect.guard.spec.ts +++ b/apps/web/src/app/admin-console/organizations/guards/org-redirect.guard.spec.ts @@ -17,18 +17,24 @@ import { UserId } from "@bitwarden/common/types/guid"; import { organizationRedirectGuard } from "./org-redirect.guard"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: "

This is the home screen!

", standalone: false, }) export class HomescreenComponent {} +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: "

This is the admin console!

", standalone: false, }) export class AdminConsoleComponent {} +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: "

This is a subroute of the admin console!

", standalone: false, diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts index b9d44c125ad..ee09143ed2f 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts @@ -36,6 +36,8 @@ import { FreeFamiliesPolicyService } from "../../../billing/services/free-famili import { OrgSwitcherComponent } from "../../../layouts/org-switcher/org-switcher.component"; import { WebLayoutModule } from "../../../layouts/web-layout.module"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-organization-layout", templateUrl: "organization-layout.component.html", diff --git a/apps/web/src/app/admin-console/organizations/manage/entity-events.component.ts b/apps/web/src/app/admin-console/organizations/manage/entity-events.component.ts index b4c5a273ac7..b4dcb9fdfac 100644 --- a/apps/web/src/app/admin-console/organizations/manage/entity-events.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/entity-events.component.ts @@ -37,6 +37,8 @@ export interface EntityEventsDialogParams { name?: string; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ imports: [SharedModule], templateUrl: "entity-events.component.html", diff --git a/apps/web/src/app/admin-console/organizations/manage/events.component.ts b/apps/web/src/app/admin-console/organizations/manage/events.component.ts index 966499c0bee..78a6d6c0dac 100644 --- a/apps/web/src/app/admin-console/organizations/manage/events.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/events.component.ts @@ -46,6 +46,8 @@ const EVENT_SYSTEM_USER_TO_TRANSLATION: Record = { [EventSystemUser.PublicApi]: "publicApi", }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "events.component.html", imports: [SharedModule, HeaderModule], diff --git a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts index 9b9be4e50b3..03a24703c0f 100644 --- a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts @@ -107,6 +107,8 @@ export const openGroupAddEditDialog = ( ); }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-group-add-edit", templateUrl: "group-add-edit.component.html", diff --git a/apps/web/src/app/admin-console/organizations/manage/groups.component.ts b/apps/web/src/app/admin-console/organizations/manage/groups.component.ts index 23e92056c95..d7dcb8a8aa2 100644 --- a/apps/web/src/app/admin-console/organizations/manage/groups.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/groups.component.ts @@ -77,6 +77,8 @@ const groupsFilter = (filter: string) => { }; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "groups.component.html", standalone: false, diff --git a/apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts b/apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts index 16543cdb58c..86d22fdf5e9 100644 --- a/apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts @@ -17,6 +17,8 @@ export type UserConfirmDialogData = { confirmUser: (publicKey: Uint8Array) => Promise; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "user-confirm.component.html", imports: [SharedModule], diff --git a/apps/web/src/app/admin-console/organizations/manage/verify-recover-delete-org.component.ts b/apps/web/src/app/admin-console/organizations/manage/verify-recover-delete-org.component.ts index f88eb82e529..001e64f48f1 100644 --- a/apps/web/src/app/admin-console/organizations/manage/verify-recover-delete-org.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/verify-recover-delete-org.component.ts @@ -12,6 +12,8 @@ import { ToastService } from "@bitwarden/components"; import { SharedModule } from "../../../shared/shared.module"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "verify-recover-delete-org.component.html", imports: [SharedModule], diff --git a/apps/web/src/app/admin-console/organizations/members/components/account-recovery/account-recovery-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/account-recovery/account-recovery-dialog.component.ts index 3240b8d707a..bb98225498f 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/account-recovery/account-recovery-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/account-recovery/account-recovery-dialog.component.ts @@ -61,6 +61,8 @@ export type AccountRecoveryDialogResultType = * given organization user. An admin will access this form when they want to * reset a user's password and log them out of sessions. */ +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ standalone: true, selector: "app-account-recovery-dialog", @@ -76,6 +78,8 @@ export type AccountRecoveryDialogResultType = ], }) export class AccountRecoveryDialogComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild(InputPasswordComponent) inputPasswordComponent: InputPasswordComponent | undefined = undefined; diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.ts index 01b0d7bc380..55385ca0ce9 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.ts @@ -36,6 +36,8 @@ type BulkConfirmDialogParams = { users: BulkUserDetails[]; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "bulk-confirm-dialog.component.html", standalone: false, diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.ts index 8fb60e85b08..0fd60b859f0 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.ts @@ -16,6 +16,8 @@ type BulkDeleteDialogParams = { users: BulkUserDetails[]; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "bulk-delete-dialog.component.html", standalone: false, diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-enable-sm-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-enable-sm-dialog.component.ts index 9132625c587..a97d595e443 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-enable-sm-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-enable-sm-dialog.component.ts @@ -20,6 +20,8 @@ export type BulkEnableSecretsManagerDialogData = { users: OrganizationUserView[]; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: `bulk-enable-sm-dialog.component.html`, standalone: false, diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove-dialog.component.ts index 5bbc6f093f0..7c95e43c8cf 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove-dialog.component.ts @@ -19,6 +19,8 @@ type BulkRemoveDialogParams = { users: BulkUserDetails[]; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "bulk-remove-dialog.component.html", standalone: false, diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.ts index ac99a9b51de..5e542de907a 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.ts @@ -15,6 +15,8 @@ type BulkRestoreDialogParams = { isRevoking: boolean; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-bulk-restore-revoke", templateUrl: "bulk-restore-revoke.component.html", diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts index 078ba6c1fd1..4f2456e1dc6 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts @@ -38,6 +38,8 @@ type BulkStatusDialogData = { successfulMessage: string; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-bulk-status", templateUrl: "bulk-status.component.html", diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts index b951f73d953..9e40e5afe37 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts @@ -104,6 +104,8 @@ export enum MemberDialogResult { Restored = "restored", } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "member-dialog.component.html", standalone: false, diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/nested-checkbox.component.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/nested-checkbox.component.ts index 9a2025c2b30..36dcb618989 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/nested-checkbox.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/nested-checkbox.component.ts @@ -7,6 +7,8 @@ import { Subject, takeUntil } from "rxjs"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-nested-checkbox", templateUrl: "nested-checkbox.component.html", @@ -15,7 +17,11 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; export class NestedCheckboxComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() parentId: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() checkboxes: FormGroup>>; get parentIndeterminate() { 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 324452499dc..59c4c4898ea 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 @@ -60,6 +60,8 @@ class MembersTableDataSource extends PeopleTableDataSource protected statusType = OrganizationUserStatusType; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "members.component.html", standalone: false, diff --git a/apps/web/src/app/admin-console/organizations/policies/base-policy-edit.component.ts b/apps/web/src/app/admin-console/organizations/policies/base-policy-edit.component.ts index 9bf0ad24b1b..54d4491156c 100644 --- a/apps/web/src/app/admin-console/organizations/policies/base-policy-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/base-policy-edit.component.ts @@ -78,7 +78,11 @@ export abstract class BasePolicyEditDefinition { */ @Directive() export abstract class BasePolicyEditComponent implements OnInit { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() policyResponse: PolicyResponse | undefined; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() policy: BasePolicyEditDefinition | undefined; /** diff --git a/apps/web/src/app/admin-console/organizations/policies/policies.component.ts b/apps/web/src/app/admin-console/organizations/policies/policies.component.ts index 7bab6f262a6..e80796fd0af 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policies.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policies.component.ts @@ -37,6 +37,8 @@ import { PolicyEditDialogComponent } from "./policy-edit-dialog.component"; import { PolicyListService } from "./policy-list.service"; import { POLICY_EDIT_REGISTER } from "./policy-register-token"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "policies.component.html", imports: [SharedModule, HeaderModule], diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/autotype-policy.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/autotype-policy.component.ts index ce62a7ff5a3..ceace60cd99 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/autotype-policy.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/autotype-policy.component.ts @@ -18,6 +18,8 @@ export class DesktopAutotypeDefaultSettingPolicy extends BasePolicyEditDefinitio return configService.getFeatureFlag$(FeatureFlag.WindowsDesktopAutotype); } } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "autotype-policy.component.html", imports: [SharedModule], diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/disable-send.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/disable-send.component.ts index 3b4df75e555..103420fbf51 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/disable-send.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/disable-send.component.ts @@ -12,6 +12,8 @@ export class DisableSendPolicy extends BasePolicyEditDefinition { component = DisableSendPolicyComponent; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "disable-send.component.html", imports: [SharedModule], diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/master-password.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/master-password.component.ts index fe3d76a0907..c1223a2004b 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/master-password.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/master-password.component.ts @@ -26,6 +26,8 @@ export class MasterPasswordPolicy extends BasePolicyEditDefinition { component = MasterPasswordPolicyComponent; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "master-password.component.html", imports: [SharedModule], diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/organization-data-ownership.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/organization-data-ownership.component.ts index 94094b76f69..d832dff158a 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/organization-data-ownership.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/organization-data-ownership.component.ts @@ -22,6 +22,8 @@ export class OrganizationDataOwnershipPolicy extends BasePolicyEditDefinition { } } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "organization-data-ownership.component.html", imports: [SharedModule], diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/password-generator.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/password-generator.component.ts index e26d37bfdf2..e3a67362cc9 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/password-generator.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/password-generator.component.ts @@ -19,6 +19,8 @@ export class PasswordGeneratorPolicy extends BasePolicyEditDefinition { component = PasswordGeneratorPolicyComponent; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "password-generator.component.html", imports: [SharedModule], diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/remove-unlock-with-pin.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/remove-unlock-with-pin.component.ts index e95ef8a1422..ac768d47d6e 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/remove-unlock-with-pin.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/remove-unlock-with-pin.component.ts @@ -12,6 +12,8 @@ export class RemoveUnlockWithPinPolicy extends BasePolicyEditDefinition { component = RemoveUnlockWithPinPolicyComponent; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "remove-unlock-with-pin.component.html", imports: [SharedModule], diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/require-sso.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/require-sso.component.ts index 3f28c0cb068..904c29ca70d 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/require-sso.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/require-sso.component.ts @@ -19,6 +19,8 @@ export class RequireSsoPolicy extends BasePolicyEditDefinition { } } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "require-sso.component.html", imports: [SharedModule], diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/reset-password.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/reset-password.component.ts index fafb0b32398..bfe149048e3 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/reset-password.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/reset-password.component.ts @@ -26,6 +26,8 @@ export class ResetPasswordPolicy extends BasePolicyEditDefinition { } } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "reset-password.component.html", imports: [SharedModule], diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/restricted-item-types.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/restricted-item-types.component.ts index 8f2573f0da3..554542f8a84 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/restricted-item-types.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/restricted-item-types.component.ts @@ -12,6 +12,8 @@ export class RestrictedItemTypesPolicy extends BasePolicyEditDefinition { component = RestrictedItemTypesPolicyComponent; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "restricted-item-types.component.html", imports: [SharedModule], diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/send-options.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/send-options.component.ts index e581ed2f4c7..b8a59e8f8ef 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/send-options.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/send-options.component.ts @@ -13,6 +13,8 @@ export class SendOptionsPolicy extends BasePolicyEditDefinition { component = SendOptionsPolicyComponent; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "send-options.component.html", imports: [SharedModule], diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/single-org.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/single-org.component.ts index ecaa86b03bc..655c5f20610 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/single-org.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/single-org.component.ts @@ -12,6 +12,8 @@ export class SingleOrgPolicy extends BasePolicyEditDefinition { component = SingleOrgPolicyComponent; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "single-org.component.html", imports: [SharedModule], diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/two-factor-authentication.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/two-factor-authentication.component.ts index 13b7660c4e7..62f3d1f3466 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/two-factor-authentication.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/two-factor-authentication.component.ts @@ -12,6 +12,8 @@ export class TwoFactorAuthenticationPolicy extends BasePolicyEditDefinition { component = TwoFactorAuthenticationPolicyComponent; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "two-factor-authentication.component.html", imports: [SharedModule], diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/vnext-organization-data-ownership.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/vnext-organization-data-ownership.component.ts index 2234d5c7437..627f5762eda 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/vnext-organization-data-ownership.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/vnext-organization-data-ownership.component.ts @@ -34,6 +34,8 @@ export class vNextOrganizationDataOwnershipPolicy extends BasePolicyEditDefiniti } } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "vnext-organization-data-ownership.component.html", imports: [SharedModule], @@ -50,6 +52,8 @@ export class vNextOrganizationDataOwnershipPolicyComponent super(); } + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("dialog", { static: true }) warningContent!: TemplateRef; override async confirm(): Promise { diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialog.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialog.component.ts index d98b5d4809b..98b6d1c6bee 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialog.component.ts @@ -45,11 +45,15 @@ export type PolicyEditDialogData = { export type PolicyEditDialogResult = "saved"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "policy-edit-dialog.component.html", imports: [SharedModule], }) export class PolicyEditDialogComponent implements AfterViewInit { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("policyForm", { read: ViewContainerRef, static: true }) policyFormRef: ViewContainerRef | undefined; diff --git a/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts b/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts index 52cb24c90d1..6043bfd3193 100644 --- a/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts +++ b/apps/web/src/app/admin-console/organizations/reporting/reports-home.component.ts @@ -14,6 +14,8 @@ import { ProductTierType } from "@bitwarden/common/billing/enums"; import { ReportVariant, reports, ReportType, ReportEntry } from "../../../dirt/reports"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-org-reports-home", templateUrl: "reports-home.component.html", diff --git a/apps/web/src/app/admin-console/organizations/settings/account.component.ts b/apps/web/src/app/admin-console/organizations/settings/account.component.ts index 21424e86521..68b220aeac0 100644 --- a/apps/web/src/app/admin-console/organizations/settings/account.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/account.component.ts @@ -38,6 +38,8 @@ import { PurgeVaultComponent } from "../../../vault/settings/purge-vault.compone import { DeleteOrganizationDialogResult, openDeleteOrganizationDialog } from "./components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-org-account", templateUrl: "account.component.html", diff --git a/apps/web/src/app/admin-console/organizations/settings/components/delete-organization-dialog.component.ts b/apps/web/src/app/admin-console/organizations/settings/components/delete-organization-dialog.component.ts index 1b41dc31a62..8cf1530cb7d 100644 --- a/apps/web/src/app/admin-console/organizations/settings/components/delete-organization-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/components/delete-organization-dialog.component.ts @@ -78,6 +78,8 @@ export enum DeleteOrganizationDialogResult { Canceled = "canceled", } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-delete-organization", imports: [SharedModule, UserVerificationModule], diff --git a/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts b/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts index 3151e0a702f..46e39a112bf 100644 --- a/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts @@ -26,6 +26,8 @@ import { TwoFactorSetupDuoComponent } from "../../../auth/settings/two-factor/tw import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from "../../../auth/settings/two-factor/two-factor-setup.component"; import { TwoFactorVerifyComponent } from "../../../auth/settings/two-factor/two-factor-verify.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-two-factor-setup", templateUrl: "../../../auth/settings/two-factor/two-factor-setup.component.html", diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.ts b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.ts index 43843314ce5..89ecfd07174 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.ts @@ -45,6 +45,8 @@ export enum PermissionMode { Edit = "edit", } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "bit-access-selector", templateUrl: "access-selector.component.html", @@ -139,6 +141,8 @@ export class AccessSelectorComponent implements ControlValueAccessor, OnInit, On /** * List of all selectable items that. Sorted internally. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() get items(): AccessItemView[] { return this.selectionList.allItems; @@ -160,6 +164,8 @@ export class AccessSelectorComponent implements ControlValueAccessor, OnInit, On /** * Permission mode that controls if the permission form controls and column should be present. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() get permissionMode(): PermissionMode { return this._permissionMode; @@ -175,41 +181,64 @@ export class AccessSelectorComponent implements ControlValueAccessor, OnInit, On /** * Column header for the selected items table */ - @Input() columnHeader: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals + @Input() + columnHeader: string; /** * Label used for the ng selector */ - @Input() selectorLabelText: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals + @Input() + selectorLabelText: string; /** * Helper text displayed under the ng selector */ - @Input() selectorHelpText: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals + @Input() + selectorHelpText: string; /** * Text that is shown in the table when no items are selected */ - @Input() emptySelectionText: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals + @Input() + emptySelectionText: string; /** * Flag for if the member roles column should be present */ - @Input() showMemberRoles: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals + @Input() + showMemberRoles: boolean; /** * Flag for if the group column should be present */ - @Input() showGroupColumn: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals + @Input() + showGroupColumn: boolean; /** * Hide the multi-select so that new items cannot be added */ - @Input() hideMultiSelect = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals + @Input() + hideMultiSelect = false; /** * The initial permission that will be selected in the dialog, defaults to View. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() protected initialPermission: CollectionPermission = CollectionPermission.View; diff --git a/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts b/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts index ea1a47d85cc..7b189270e1b 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts @@ -116,6 +116,8 @@ export enum CollectionDialogAction { Upgrade = "upgrade", } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "collection-dialog.component.html", imports: [SharedModule, AccessSelectorModule, SelectModule], diff --git a/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.ts b/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.ts index c4fe0350006..c34073b2a04 100644 --- a/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.ts +++ b/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.ts @@ -18,6 +18,8 @@ import { BaseAcceptComponent } from "../../../common/base.accept.component"; * "Bitwarden allows all members of Enterprise Organizations to redeem a complimentary Families Plan with their * personal email address." - https://bitwarden.com/learning/free-families-plan-for-enterprise/ */ +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "accept-family-sponsorship.component.html", imports: [CommonModule, I18nPipe, IconModule], diff --git a/apps/web/src/app/admin-console/organizations/sponsorships/families-for-enterprise-setup.component.ts b/apps/web/src/app/admin-console/organizations/sponsorships/families-for-enterprise-setup.component.ts index 30c0ba159c1..3c400decd52 100644 --- a/apps/web/src/app/admin-console/organizations/sponsorships/families-for-enterprise-setup.component.ts +++ b/apps/web/src/app/admin-console/organizations/sponsorships/families-for-enterprise-setup.component.ts @@ -28,11 +28,15 @@ import { openDeleteOrganizationDialog, } from "../settings/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "families-for-enterprise-setup.component.html", imports: [SharedModule, OrganizationPlansComponent], }) export class FamiliesForEnterpriseSetupComponent implements OnInit, OnDestroy { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild(OrganizationPlansComponent, { static: false }) set organizationPlansComponent(value: OrganizationPlansComponent) { if (!value) { diff --git a/apps/web/src/app/admin-console/settings/create-organization.component.ts b/apps/web/src/app/admin-console/settings/create-organization.component.ts index f87e9ec5b72..bdf450fb265 100644 --- a/apps/web/src/app/admin-console/settings/create-organization.component.ts +++ b/apps/web/src/app/admin-console/settings/create-organization.component.ts @@ -11,6 +11,8 @@ import { OrganizationPlansComponent } from "../../billing"; import { HeaderModule } from "../../layouts/header/header.module"; import { SharedModule } from "../../shared"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "create-organization.component.html", imports: [SharedModule, OrganizationPlansComponent, HeaderModule], diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.ts index 258a112e234..3c2adb46193 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.ts @@ -24,6 +24,8 @@ import { KeyService } from "@bitwarden/key-management"; import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; import { SharedModule } from "@bitwarden/web-vault/app/shared/shared.module"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-org-device-approvals", templateUrl: "./device-approvals.component.html", diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.ts index 970a476df22..e3e5a927369 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.ts @@ -22,6 +22,8 @@ export interface DomainAddEditDialogData { existingDomainNames: Array; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "domain-add-edit-dialog.component.html", standalone: false, diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.ts index 3bc916d3fc5..bfe382f930e 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.ts @@ -33,6 +33,8 @@ import { DomainAddEditDialogData, } from "./domain-add-edit-dialog/domain-add-edit-dialog.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-org-manage-domain-verification", templateUrl: "domain-verification.component.html", diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.ts index de870cdbdcb..9e7f35a8475 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.ts @@ -21,6 +21,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService, ToastService } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-org-manage-scim", templateUrl: "scim.component.html", diff --git a/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/activate-autofill.component.ts b/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/activate-autofill.component.ts index 17efc017136..c32eb3d935b 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/activate-autofill.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/activate-autofill.component.ts @@ -21,6 +21,8 @@ export class ActivateAutofillPolicy extends BasePolicyEditDefinition { } } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "activate-autofill.component.html", imports: [SharedModule], diff --git a/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/automatic-app-login.component.ts b/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/automatic-app-login.component.ts index 7dadc04c6f4..85110a5af21 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/automatic-app-login.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/automatic-app-login.component.ts @@ -17,6 +17,8 @@ export class AutomaticAppLoginPolicy extends BasePolicyEditDefinition { component = AutomaticAppLoginPolicyComponent; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "automatic-app-login.component.html", imports: [SharedModule], diff --git a/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/disable-personal-vault-export.component.ts b/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/disable-personal-vault-export.component.ts index d93fb50b0e2..17e8eb055b5 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/disable-personal-vault-export.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/disable-personal-vault-export.component.ts @@ -14,6 +14,8 @@ export class DisablePersonalVaultExportPolicy extends BasePolicyEditDefinition { component = DisablePersonalVaultExportPolicyComponent; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "disable-personal-vault-export.component.html", imports: [SharedModule], diff --git a/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/maximum-vault-timeout.component.ts b/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/maximum-vault-timeout.component.ts index 160ce9aeb20..277388e2883 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/maximum-vault-timeout.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/maximum-vault-timeout.component.ts @@ -20,6 +20,8 @@ export class MaximumVaultTimeoutPolicy extends BasePolicyEditDefinition { component = MaximumVaultTimeoutPolicyComponent; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "maximum-vault-timeout.component.html", imports: [SharedModule], diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.ts index 9f28ba87186..b673dfd1b14 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.ts @@ -11,6 +11,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { BaseAcceptComponent } from "@bitwarden/web-vault/app/common/base.accept.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-accept-provider", templateUrl: "accept-provider.component.html", diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/add-edit-member-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/add-edit-member-dialog.component.ts index e21837f7226..635aaf16b3f 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/add-edit-member-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/add-edit-member-dialog.component.ts @@ -33,6 +33,8 @@ export enum AddEditMemberDialogResultType { Saved = "saved", } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "add-edit-member-dialog.component.html", standalone: false, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts index 8bbc299269d..dd54b842062 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts @@ -26,6 +26,8 @@ type BulkConfirmDialogParams = { users: BulkUserDetails[]; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "../../../../../../../../apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.html", diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-remove-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-remove-dialog.component.ts index e000d918414..29b50f71c1b 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-remove-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-remove-dialog.component.ts @@ -16,6 +16,8 @@ type BulkRemoveDialogParams = { users: BulkUserDetails[]; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "../../../../../../../../apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove-dialog.component.html", diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.ts index 43fc958585a..3d00d897175 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/events.component.ts @@ -20,6 +20,8 @@ import { BaseEventsComponent } from "@bitwarden/web-vault/app/admin-console/comm import { EventService } from "@bitwarden/web-vault/app/core"; import { EventExportService } from "@bitwarden/web-vault/app/tools/event-export"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "provider-events", templateUrl: "events.component.html", 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 e86956dec93..b1cd52cf8a6 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 @@ -46,6 +46,8 @@ class MembersTableDataSource extends PeopleTableDataSource { protected statusType = ProviderUserStatusType; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "members.component.html", standalone: false, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts index da82742ddd5..2e0cf2163a4 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts @@ -23,6 +23,8 @@ import { WebLayoutModule } from "@bitwarden/web-vault/app/layouts/web-layout.mod import { ProviderWarningsService } from "../../billing/providers/warnings/services"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "providers-layout", templateUrl: "providers-layout.component.html", diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.component.ts index d13ac863437..aa79ec7e29e 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.component.ts @@ -10,6 +10,8 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-providers", templateUrl: "providers.component.html", diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts index 12dada12aa9..705069dc697 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts @@ -17,6 +17,8 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { DialogService, ToastService } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "provider-account", templateUrl: "account.component.html", diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup-provider.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup-provider.component.ts index 02ca72fa9b8..fa75f4b7635 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup-provider.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup-provider.component.ts @@ -4,6 +4,8 @@ import { Params } from "@angular/router"; import { BitwardenLogo } from "@bitwarden/assets/svg"; import { BaseAcceptComponent } from "@bitwarden/web-vault/app/common/base.accept.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-setup-provider", templateUrl: "setup-provider.component.html", diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts index 0fa69c7a0e6..87c48608b10 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts @@ -20,12 +20,16 @@ import { getBillingAddressFromForm, } from "@bitwarden/web-vault/app/billing/payment/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "provider-setup", templateUrl: "setup.component.html", standalone: false, }) export class SetupComponent implements OnInit, OnDestroy { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild(EnterPaymentMethodComponent) enterPaymentMethodComponent!: EnterPaymentMethodComponent; loading = true; diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts index 5c0d0982fb5..f1be766a9a2 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts @@ -10,6 +10,8 @@ import { ProviderVerifyRecoverDeleteRequest } from "@bitwarden/common/admin-cons import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ToastService } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-verify-recover-delete-provider", templateUrl: "verify-recover-delete-provider.component.html", From bb07365ea5b9bf2c70649109be5157df220a5d47 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 23 Oct 2025 10:34:16 -0500 Subject: [PATCH 10/73] await call that creates Customer in case we're upgrading from free (#16999) --- .../billing/organizations/change-plan-dialog.component.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts index 9d093ec4514..c2c819ddf4d 100644 --- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts @@ -842,10 +842,9 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { ); const subscriber: BitwardenSubscriber = { type: "organization", data: this.organization }; - await Promise.all([ - this.subscriberBillingClient.updatePaymentMethod(subscriber, paymentMethod, null), - this.subscriberBillingClient.updateBillingAddress(subscriber, billingAddress), - ]); + // These need to be synchronous so one of them can create the Customer in the case we're upgrading from Free. + await this.subscriberBillingClient.updateBillingAddress(subscriber, billingAddress); + await this.subscriberBillingClient.updatePaymentMethod(subscriber, paymentMethod, null); } // Backfill pub/priv key if necessary From d91fdad0118d66a3c54ea0839f98b59a04b09f88 Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Thu, 23 Oct 2025 11:54:20 -0400 Subject: [PATCH 11/73] [PM-24650] Resolve sign in button disappearing from ADP login form (#16901) * ensure autofillInsertActions execution order is preserved * don't fill a field if it already has the value that is going to be filled * update tests --- .../insert-autofill-content.service.spec.ts | 54 ++++++++++++++----- .../insert-autofill-content.service.ts | 9 +++- 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts b/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts index 9edcdbb3a95..07fdfb9db79 100644 --- a/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts +++ b/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts @@ -103,7 +103,7 @@ describe("InsertAutofillContentService", () => { delay_between_operations: 20, }, metadata: {}, - autosubmit: null, + autosubmit: [], savedUrls: ["https://bitwarden.com"], untrustedIframe: false, itemType: "login", @@ -218,28 +218,21 @@ describe("InsertAutofillContentService", () => { await insertAutofillContentService.fillForm(fillScript); - expect(insertAutofillContentService["userCancelledInsecureUrlAutofill"]).toHaveBeenCalled(); - expect( - insertAutofillContentService["userCancelledUntrustedIframeAutofill"], - ).toHaveBeenCalled(); expect(insertAutofillContentService["runFillScriptAction"]).toHaveBeenCalledTimes(3); expect(insertAutofillContentService["runFillScriptAction"]).toHaveBeenNthCalledWith( 1, fillScript.script[0], 0, - fillScript.script, ); expect(insertAutofillContentService["runFillScriptAction"]).toHaveBeenNthCalledWith( 2, fillScript.script[1], 1, - fillScript.script, ); expect(insertAutofillContentService["runFillScriptAction"]).toHaveBeenNthCalledWith( 3, fillScript.script[2], 2, - fillScript.script, ); }); }); @@ -623,14 +616,12 @@ describe("InsertAutofillContentService", () => { }); }); - it("will set the `value` attribute of any passed input or textarea elements", () => { - document.body.innerHTML = ``; + it("will set the `value` attribute of any passed input or textarea elements if the value differs", () => { + document.body.innerHTML = ``; const value1 = "test"; const value2 = "test2"; const textInputElement = document.getElementById("username") as HTMLInputElement; - textInputElement.value = value1; const textareaElement = document.getElementById("bio") as HTMLTextAreaElement; - textareaElement.value = value2; jest.spyOn(insertAutofillContentService as any, "handleInsertValueAndTriggerSimulatedEvents"); insertAutofillContentService["insertValueIntoField"](textInputElement, value1); @@ -647,6 +638,45 @@ describe("InsertAutofillContentService", () => { insertAutofillContentService["handleInsertValueAndTriggerSimulatedEvents"], ).toHaveBeenCalledWith(textareaElement, expect.any(Function)); }); + + it("will NOT set the `value` attribute of any passed input or textarea elements if they already have values matching the passed value", () => { + document.body.innerHTML = ``; + const value1 = "test"; + const value2 = "test2"; + const textInputElement = document.getElementById("username") as HTMLInputElement; + textInputElement.value = value1; + const textareaElement = document.getElementById("bio") as HTMLTextAreaElement; + textareaElement.value = value2; + jest.spyOn(insertAutofillContentService as any, "handleInsertValueAndTriggerSimulatedEvents"); + + insertAutofillContentService["insertValueIntoField"](textInputElement, value1); + + expect(textInputElement.value).toBe(value1); + expect( + insertAutofillContentService["handleInsertValueAndTriggerSimulatedEvents"], + ).not.toHaveBeenCalled(); + + insertAutofillContentService["insertValueIntoField"](textareaElement, value2); + + expect(textareaElement.value).toBe(value2); + expect( + insertAutofillContentService["handleInsertValueAndTriggerSimulatedEvents"], + ).not.toHaveBeenCalled(); + }); + + it("skips filling when the field already has the target value", () => { + const value = "test"; + document.body.innerHTML = ``; + const element = document.getElementById("username") as FillableFormFieldElement; + jest.spyOn(insertAutofillContentService as any, "handleInsertValueAndTriggerSimulatedEvents"); + + insertAutofillContentService["insertValueIntoField"](element, value); + + expect( + insertAutofillContentService["handleInsertValueAndTriggerSimulatedEvents"], + ).not.toHaveBeenCalled(); + expect(element.value).toBe(value); + }); }); describe("handleInsertValueAndTriggerSimulatedEvents", () => { diff --git a/apps/browser/src/autofill/services/insert-autofill-content.service.ts b/apps/browser/src/autofill/services/insert-autofill-content.service.ts index 6034563a947..9ddbcdc005d 100644 --- a/apps/browser/src/autofill/services/insert-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/insert-autofill-content.service.ts @@ -49,8 +49,9 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf return; } - const fillActionPromises = fillScript.script.map(this.runFillScriptAction); - await Promise.all(fillActionPromises); + for (let index = 0; index < fillScript.script.length; index++) { + await this.runFillScriptAction(fillScript.script[index], index); + } } /** @@ -189,10 +190,14 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf const elementCanBeReadonly = elementIsInputElement(element) || elementIsTextAreaElement(element); const elementCanBeFilled = elementCanBeReadonly || elementIsSelectElement(element); + const elementValue = (element as HTMLInputElement)?.value || element?.innerText || ""; + + const elementAlreadyHasTheValue = !!(elementValue?.length && elementValue === value); if ( !element || !value || + elementAlreadyHasTheValue || (elementCanBeReadonly && element.readOnly) || (elementCanBeFilled && element.disabled) ) { From 660e452ba1de0c0c6dcea5665b859b690c79bd7e Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Thu, 23 Oct 2025 16:59:57 +0100 Subject: [PATCH 12/73] [PM-25858]Organization warnings endpoint should not be called from self-hosted instances (#16781) * ensure that getWarnings from server is not called for selfhost * Refactor the code * move the selfhost check to getWarning message * Fix the failing test --- .../organization-warnings.service.spec.ts | 58 +++++++++++++++++++ .../services/organization-warnings.service.ts | 12 +++- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.spec.ts b/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.spec.ts index 8c2a7634264..9466e813e4d 100644 --- a/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.spec.ts +++ b/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.spec.ts @@ -16,6 +16,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { ProductTierType } from "@bitwarden/common/billing/enums"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogRef, DialogService } from "@bitwarden/components"; import { OrganizationBillingClient } from "@bitwarden/web-vault/app/billing/clients"; import { @@ -37,6 +38,7 @@ describe("OrganizationWarningsService", () => { let i18nService: MockProxy; let organizationApiService: MockProxy; let organizationBillingClient: MockProxy; + let platformUtilsService: MockProxy; let router: MockProxy; const organization = { @@ -58,10 +60,13 @@ describe("OrganizationWarningsService", () => { i18nService = mock(); organizationApiService = mock(); organizationBillingClient = mock(); + platformUtilsService = mock(); router = mock(); (openChangePlanDialog as jest.Mock).mockReset(); + platformUtilsService.isSelfHost.mockReturnValue(false); + i18nService.t.mockImplementation((key: string, ...args: any[]) => { switch (key) { case "freeTrialEndPromptCount": @@ -94,6 +99,7 @@ describe("OrganizationWarningsService", () => { { provide: I18nService, useValue: i18nService }, { provide: OrganizationApiServiceAbstraction, useValue: organizationApiService }, { provide: OrganizationBillingClient, useValue: organizationBillingClient }, + { provide: PlatformUtilsService, useValue: platformUtilsService }, { provide: Router, useValue: router }, ], }); @@ -111,6 +117,16 @@ describe("OrganizationWarningsService", () => { }); }); + it("should return null when platform is self-hosted", (done) => { + platformUtilsService.isSelfHost.mockReturnValue(true); + + service.getFreeTrialWarning$(organization).subscribe((result) => { + expect(result).toBeNull(); + expect(organizationBillingClient.getWarnings).not.toHaveBeenCalled(); + done(); + }); + }); + it("should return warning with count message when remaining trial days >= 2", (done) => { const warning = { remainingTrialDays: 5 }; organizationBillingClient.getWarnings.mockResolvedValue({ @@ -206,6 +222,16 @@ describe("OrganizationWarningsService", () => { }); }); + it("should return null when platform is self-hosted", (done) => { + platformUtilsService.isSelfHost.mockReturnValue(true); + + service.getResellerRenewalWarning$(organization).subscribe((result) => { + expect(result).toBeNull(); + expect(organizationBillingClient.getWarnings).not.toHaveBeenCalled(); + done(); + }); + }); + it("should return upcoming warning with correct type and message", (done) => { const renewalDate = new Date(2024, 11, 31); const warning = { @@ -298,6 +324,16 @@ describe("OrganizationWarningsService", () => { }); }); + it("should return null when platform is self-hosted", (done) => { + platformUtilsService.isSelfHost.mockReturnValue(true); + + service.getTaxIdWarning$(organization).subscribe((result) => { + expect(result).toBeNull(); + expect(organizationBillingClient.getWarnings).not.toHaveBeenCalled(); + done(); + }); + }); + it("should return tax_id_missing type when tax ID is missing", (done) => { const warning = { type: TaxIdWarningTypes.Missing }; organizationBillingClient.getWarnings.mockResolvedValue({ @@ -427,6 +463,16 @@ describe("OrganizationWarningsService", () => { }); }); + it("should not show dialog when platform is self-hosted", (done) => { + platformUtilsService.isSelfHost.mockReturnValue(true); + + service.showInactiveSubscriptionDialog$(organization).subscribe(() => { + expect(dialogService.openSimpleDialog).not.toHaveBeenCalled(); + expect(organizationBillingClient.getWarnings).not.toHaveBeenCalled(); + done(); + }); + }); + it("should show contact provider dialog for contact_provider resolution", (done) => { const warning = { resolution: "contact_provider" }; organizationBillingClient.getWarnings.mockResolvedValue({ @@ -570,6 +616,18 @@ describe("OrganizationWarningsService", () => { }); }); + it("should not show dialog when platform is self-hosted", (done) => { + platformUtilsService.isSelfHost.mockReturnValue(true); + + service.showSubscribeBeforeFreeTrialEndsDialog$(organization).subscribe({ + complete: () => { + expect(organizationApiService.getSubscription).not.toHaveBeenCalled(); + expect(organizationBillingClient.getWarnings).not.toHaveBeenCalled(); + done(); + }, + }); + }); + it("should open trial payment dialog when free trial warning exists", (done) => { const warning = { remainingTrialDays: 2 }; const subscription = { id: "sub-123" } as OrganizationSubscriptionResponse; diff --git a/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.ts b/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.ts index 8bec7acffe1..a34533bcada 100644 --- a/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.ts +++ b/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.ts @@ -8,6 +8,7 @@ import { map, merge, Observable, + of, Subject, switchMap, tap, @@ -17,6 +18,7 @@ import { take } from "rxjs/operators"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { OrganizationId } from "@bitwarden/common/types/guid"; import { DialogService } from "@bitwarden/components"; import { OrganizationBillingClient } from "@bitwarden/web-vault/app/billing/clients"; @@ -56,6 +58,7 @@ export class OrganizationWarningsService { private i18nService: I18nService, private organizationApiService: OrganizationApiServiceAbstraction, private organizationBillingClient: OrganizationBillingClient, + private platformUtilsService: PlatformUtilsService, private router: Router, ) {} @@ -281,12 +284,17 @@ export class OrganizationWarningsService { organization: Organization, extract: (response: OrganizationWarningsResponse) => T | null | undefined, bypassCache: boolean = false, - ): Observable => - this.readThroughWarnings$(organization, bypassCache).pipe( + ): Observable => { + if (this.platformUtilsService.isSelfHost()) { + return of(null); + } + + return this.readThroughWarnings$(organization, bypassCache).pipe( map((response) => { const value = extract(response); return value ? value : null; }), take(1), ); + }; } From 2c13236550652963e13a26c2102da0cd14b1e3ec Mon Sep 17 00:00:00 2001 From: neuronull <9162534+neuronull@users.noreply.github.com> Date: Thu, 23 Oct 2025 09:42:48 -0700 Subject: [PATCH 13/73] Add desktop autotype unittests for windows (#16710) * Add desktop autotype unittests for windows * lint * fix TODO comment * feedback coltonhurst: rename trait --- apps/desktop/desktop_native/Cargo.lock | 112 +++++++ .../desktop_native/autotype/Cargo.toml | 2 + .../desktop_native/autotype/src/lib.rs | 7 +- .../autotype/src/windows/mod.rs | 41 +++ .../src/{windows.rs => windows/type_input.rs} | 250 +++++++-------- .../autotype/src/windows/window_title.rs | 298 ++++++++++++++++++ 6 files changed, 565 insertions(+), 145 deletions(-) create mode 100644 apps/desktop/desktop_native/autotype/src/windows/mod.rs rename apps/desktop/desktop_native/autotype/src/{windows.rs => windows/type_input.rs} (57%) create mode 100644 apps/desktop/desktop_native/autotype/src/windows/window_title.rs diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 3df6b41734b..5dec59f0f12 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -343,6 +343,8 @@ name = "autotype" version = "0.0.0" dependencies = [ "anyhow", + "mockall", + "serial_test", "tracing", "windows 0.61.1", "windows-core 0.61.0", @@ -1070,6 +1072,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562" +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + [[package]] name = "downcast-rs" version = "1.2.1" @@ -1288,6 +1296,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fragile" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" + [[package]] name = "fs-err" version = "2.11.0" @@ -1943,6 +1957,32 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "mockall" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "napi" version = "2.16.17" @@ -2575,6 +2615,32 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "predicates" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +dependencies = [ + "anstyle", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" + +[[package]] +name = "predicates-tree" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "primeorder" version = "0.13.6" @@ -2877,6 +2943,15 @@ dependencies = [ "cipher", ] +[[package]] +name = "scc" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" +dependencies = [ + "sdd", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -2920,6 +2995,12 @@ dependencies = [ "sha2", ] +[[package]] +name = "sdd" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" + [[package]] name = "sec1" version = "0.7.3" @@ -3024,6 +3105,31 @@ dependencies = [ "syn", ] +[[package]] +name = "serial_test" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" +dependencies = [ + "futures", + "log", + "once_cell", + "parking_lot", + "scc", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha1" version = "0.10.6" @@ -3263,6 +3369,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + [[package]] name = "textwrap" version = "0.16.2" diff --git a/apps/desktop/desktop_native/autotype/Cargo.toml b/apps/desktop/desktop_native/autotype/Cargo.toml index 3d1e74254ce..267074d0bc8 100644 --- a/apps/desktop/desktop_native/autotype/Cargo.toml +++ b/apps/desktop/desktop_native/autotype/Cargo.toml @@ -9,6 +9,8 @@ publish.workspace = true anyhow = { workspace = true } [target.'cfg(windows)'.dependencies] +mockall = "=0.13.1" +serial_test = "=3.2.0" tracing.workspace = true windows = { workspace = true, features = [ "Win32_UI_Input_KeyboardAndMouse", diff --git a/apps/desktop/desktop_native/autotype/src/lib.rs b/apps/desktop/desktop_native/autotype/src/lib.rs index 92996996434..c87fea23b60 100644 --- a/apps/desktop/desktop_native/autotype/src/lib.rs +++ b/apps/desktop/desktop_native/autotype/src/lib.rs @@ -2,7 +2,7 @@ use anyhow::Result; #[cfg_attr(target_os = "linux", path = "linux.rs")] #[cfg_attr(target_os = "macos", path = "macos.rs")] -#[cfg_attr(target_os = "windows", path = "windows.rs")] +#[cfg_attr(target_os = "windows", path = "windows/mod.rs")] mod windowing; /// Gets the title bar string for the foreground window. @@ -20,12 +20,13 @@ pub fn get_foreground_window_title() -> Result { /// /// # Arguments /// -/// * `input` must be an array of utf-16 encoded characters to insert. +/// * `input` an array of utf-16 encoded characters to insert. +/// * `keyboard_shortcut` a vector of valid shortcut keys: Control, Alt, Super, Shift, letters a - Z /// /// # Errors /// /// This function returns an `anyhow::Error` if there is any -/// issue obtaining the window title. Detailed reasons will +/// issue in typing the input. Detailed reasons will /// vary based on platform implementation. pub fn type_input(input: Vec, keyboard_shortcut: Vec) -> Result<()> { windowing::type_input(input, keyboard_shortcut) diff --git a/apps/desktop/desktop_native/autotype/src/windows/mod.rs b/apps/desktop/desktop_native/autotype/src/windows/mod.rs new file mode 100644 index 00000000000..3ea63b2b8f4 --- /dev/null +++ b/apps/desktop/desktop_native/autotype/src/windows/mod.rs @@ -0,0 +1,41 @@ +use anyhow::Result; +use tracing::debug; +use windows::Win32::Foundation::{GetLastError, SetLastError, WIN32_ERROR}; + +mod type_input; +mod window_title; + +/// The error code from Win32 API that represents a non-error. +const WIN32_SUCCESS: WIN32_ERROR = WIN32_ERROR(0); + +/// `ErrorOperations` provides an interface to the Win32 API for dealing with +/// win32 errors. +#[cfg_attr(test, mockall::automock)] +trait ErrorOperations { + /// https://learn.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-setlasterror + fn set_last_error(err: u32) { + debug!(err, "Calling SetLastError"); + unsafe { + SetLastError(WIN32_ERROR(err)); + } + } + + /// https://learn.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-getlasterror + fn get_last_error() -> WIN32_ERROR { + let last_err = unsafe { GetLastError() }; + debug!("GetLastError(): {}", last_err.to_hresult().message()); + last_err + } +} + +/// Default implementation for Win32 API errors. +struct Win32ErrorOperations; +impl ErrorOperations for Win32ErrorOperations {} + +pub fn get_foreground_window_title() -> Result { + window_title::get_foreground_window_title() +} + +pub fn type_input(input: Vec, keyboard_shortcut: Vec) -> Result<()> { + type_input::type_input(input, keyboard_shortcut) +} diff --git a/apps/desktop/desktop_native/autotype/src/windows.rs b/apps/desktop/desktop_native/autotype/src/windows/type_input.rs similarity index 57% rename from apps/desktop/desktop_native/autotype/src/windows.rs rename to apps/desktop/desktop_native/autotype/src/windows/type_input.rs index 01270e7971d..b757cf7752f 100644 --- a/apps/desktop/desktop_native/autotype/src/windows.rs +++ b/apps/desktop/desktop_native/autotype/src/windows/type_input.rs @@ -1,136 +1,42 @@ -use std::{ffi::OsString, os::windows::ffi::OsStringExt}; - use anyhow::{anyhow, Result}; -use tracing::{debug, error, warn}; -use windows::Win32::{ - Foundation::{GetLastError, SetLastError, HWND, WIN32_ERROR}, - UI::{ - Input::KeyboardAndMouse::{ - SendInput, INPUT, INPUT_0, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_KEYUP, - KEYEVENTF_UNICODE, VIRTUAL_KEY, - }, - WindowsAndMessaging::{GetForegroundWindow, GetWindowTextLengthW, GetWindowTextW}, - }, +use tracing::{debug, error}; +use windows::Win32::UI::Input::KeyboardAndMouse::{ + SendInput, INPUT, INPUT_0, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_KEYUP, KEYEVENTF_UNICODE, + VIRTUAL_KEY, }; -const WIN32_SUCCESS: WIN32_ERROR = WIN32_ERROR(0); +use super::{ErrorOperations, Win32ErrorOperations}; -fn clear_last_error() { - debug!("Clearing last error with SetLastError."); - unsafe { - SetLastError(WIN32_ERROR(0)); +/// `InputOperations` provides an interface to Window32 API for +/// working with inputs. +#[cfg_attr(test, mockall::automock)] +trait InputOperations { + /// Attempts to type the provided input wherever the user's cursor is. + /// + /// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput + fn send_input(inputs: &[INPUT]) -> u32; +} + +struct Win32InputOperations; + +impl InputOperations for Win32InputOperations { + fn send_input(inputs: &[INPUT]) -> u32 { + const INPUT_STRUCT_SIZE: i32 = std::mem::size_of::() as i32; + let insert_count = unsafe { SendInput(inputs, INPUT_STRUCT_SIZE) }; + + debug!(insert_count, "SendInput() called."); + + insert_count } } -fn get_last_error() -> WIN32_ERROR { - let last_err = unsafe { GetLastError() }; - debug!("GetLastError(): {}", last_err.to_hresult().message()); - last_err -} - -// The handle should be validated before any unsafe calls referencing it. -fn validate_window_handle(handle: &HWND) -> Result<()> { - if handle.is_invalid() { - error!("Window handle is invalid."); - return Err(anyhow!("Window handle is invalid.")); - } - Ok(()) -} - -// ---------- Window title -------------- - -/// Gets the title bar string for the foreground window. -pub fn get_foreground_window_title() -> Result { - // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getforegroundwindow - let window_handle = unsafe { GetForegroundWindow() }; - - debug!("GetForegroundWindow() called."); - - validate_window_handle(&window_handle)?; - - get_window_title(&window_handle) -} - -/// Gets the length of the window title bar text. -/// -/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtextlengthw -fn get_window_title_length(window_handle: &HWND) -> Result { - // GetWindowTextLengthW does not itself clear the last error so we must do it ourselves. - clear_last_error(); - - validate_window_handle(window_handle)?; - - let length = unsafe { GetWindowTextLengthW(*window_handle) }; - - let length = usize::try_from(length)?; - - debug!(length, "window text length retrieved from handle."); - - if length == 0 { - // attempt to retreive win32 error - let last_err = get_last_error(); - if last_err != WIN32_SUCCESS { - let last_err = last_err.to_hresult().message(); - error!(last_err, "Error getting window text length."); - return Err(anyhow!("Error getting window text length: {last_err}")); - } - } - - Ok(length) -} - -/// Gets the window title bar title. -/// -/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtextw -fn get_window_title(window_handle: &HWND) -> Result { - let expected_window_title_length = get_window_title_length(window_handle)?; - - // This isn't considered an error by the windows API, but in practice it means we can't - // match against the title so we'll stop here. - // The upstream will make a contains comparison on what we return, so an empty string - // will not result on a match. - if expected_window_title_length == 0 { - warn!("Window title length is zero."); - return Ok(String::from("")); - } - - let mut buffer: Vec = vec![0; expected_window_title_length + 1]; // add extra space for the null character - - validate_window_handle(window_handle)?; - - let actual_window_title_length = unsafe { GetWindowTextW(*window_handle, &mut buffer) }; - - debug!(actual_window_title_length, "window title retrieved."); - - if actual_window_title_length == 0 { - // attempt to retreive win32 error - let last_err = get_last_error(); - if last_err != WIN32_SUCCESS { - let last_err = last_err.to_hresult().message(); - error!(last_err, "Error retrieving window title."); - return Err(anyhow!("Error retrieving window title. {last_err}")); - } - // in practice, we should not get to the below code, since we asserted the len > 0 - // above. but it is an extra protection in case the windows API didn't set an error. - warn!(expected_window_title_length, "No window title retrieved."); - } - - let window_title = OsString::from_wide(&buffer); - - Ok(window_title.to_string_lossy().into_owned()) -} - -// ---------- Type Input -------------- - /// Attempts to type the input text wherever the user's cursor is. /// /// `input` must be a vector of utf-16 encoded characters to insert. /// `keyboard_shortcut` must be a vector of Strings, where valid shortcut keys: Control, Alt, Super, Shift, letters a - Z /// /// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput -pub fn type_input(input: Vec, keyboard_shortcut: Vec) -> Result<()> { - const TAB_KEY: u8 = 9; - +pub(super) fn type_input(input: Vec, keyboard_shortcut: Vec) -> Result<()> { // the length of this vec is always shortcut keys to release + (2x length of input chars) let mut keyboard_inputs: Vec = Vec::with_capacity(keyboard_shortcut.len() + (input.len() * 2)); @@ -142,25 +48,31 @@ pub fn type_input(input: Vec, keyboard_shortcut: Vec) -> Result<()> keyboard_inputs.push(convert_shortcut_key_to_up_input(key)?); } - // Add key "down" and "up" inputs for the input - // (currently in this form: {username}/t{password}) + add_input(&input, &mut keyboard_inputs); + + send_input::(keyboard_inputs) +} + +// Add key "down" and "up" inputs for the input +// (currently in this form: {username}/t{password}) +fn add_input(input: &[u16], keyboard_inputs: &mut Vec) { + const TAB_KEY: u8 = 9; + for i in input { - let next_down_input = if i == TAB_KEY.into() { - build_virtual_key_input(InputKeyPress::Down, i as u8) + let next_down_input = if *i == TAB_KEY.into() { + build_virtual_key_input(InputKeyPress::Down, *i as u8) } else { - build_unicode_input(InputKeyPress::Down, i) + build_unicode_input(InputKeyPress::Down, *i) }; - let next_up_input = if i == TAB_KEY.into() { - build_virtual_key_input(InputKeyPress::Up, i as u8) + let next_up_input = if *i == TAB_KEY.into() { + build_virtual_key_input(InputKeyPress::Up, *i as u8) } else { - build_unicode_input(InputKeyPress::Up, i) + build_unicode_input(InputKeyPress::Up, *i) }; keyboard_inputs.push(next_down_input); keyboard_inputs.push(next_up_input); } - - send_input(keyboard_inputs) } /// Converts a valid shortcut key to an "up" keyboard input. @@ -294,21 +206,20 @@ fn build_virtual_key_input(key_press: InputKeyPress, virtual_key: u8) -> INPUT { } } -/// Attempts to type the provided input wherever the user's cursor is. -/// -/// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput -fn send_input(inputs: Vec) -> Result<()> { - let insert_count = unsafe { SendInput(&inputs, std::mem::size_of::() as i32) }; - - debug!("SendInput() called."); +fn send_input(inputs: Vec) -> Result<()> +where + I: InputOperations, + E: ErrorOperations, +{ + let insert_count = I::send_input(&inputs); if insert_count == 0 { - let last_err = get_last_error().to_hresult().message(); + let last_err = E::get_last_error().to_hresult().message(); error!(GetLastError = %last_err, "SendInput sent 0 inputs. Input was blocked by another thread."); return Err(anyhow!("SendInput sent 0 inputs. Input was blocked by another thread. GetLastError: {last_err}")); } else if insert_count != inputs.len() as u32 { - let last_err = get_last_error().to_hresult().message(); + let last_err = E::get_last_error().to_hresult().message(); error!(sent = %insert_count, expected = inputs.len(), GetLastError = %last_err, "SendInput sent does not match expected." ); @@ -318,17 +229,23 @@ fn send_input(inputs: Vec) -> Result<()> { )); } - debug!(insert_count, "Autotype sent input."); - Ok(()) } #[cfg(test)] mod tests { + //! For the mocking of the traits that are static methods, we need to use the `serial_test` crate + //! in order to mock those, since the mock expectations set have to be global in absence of a `self`. + //! More info: https://docs.rs/mockall/latest/mockall/#static-methods + use super::*; + use crate::windowing::MockErrorOperations; + use serial_test::serial; + use windows::Win32::Foundation::WIN32_ERROR; + #[test] - fn get_alphabetic_hot_key_happy() { + fn get_alphabetic_hot_key_succeeds() { for c in ('a'..='z').chain('A'..='Z') { let letter = c.to_string(); let converted = get_alphabetic_hotkey(letter).unwrap(); @@ -349,4 +266,53 @@ mod tests { let letter = String::from("}"); get_alphabetic_hotkey(letter).unwrap(); } + + #[test] + #[serial] + fn send_input_succeeds() { + let ctxi = MockInputOperations::send_input_context(); + ctxi.expect().returning(|_| 1); + + send_input::(vec![build_unicode_input( + InputKeyPress::Up, + 0, + )]) + .unwrap(); + } + + #[test] + #[serial] + #[should_panic( + expected = "SendInput sent 0 inputs. Input was blocked by another thread. GetLastError:" + )] + fn send_input_fails_sent_zero() { + let ctxi = MockInputOperations::send_input_context(); + ctxi.expect().returning(|_| 0); + + let ctxge = MockErrorOperations::get_last_error_context(); + ctxge.expect().returning(|| WIN32_ERROR(1)); + + send_input::(vec![build_unicode_input( + InputKeyPress::Up, + 0, + )]) + .unwrap(); + } + + #[test] + #[serial] + #[should_panic(expected = "SendInput does not match expected. sent: 2, expected: 1")] + fn send_input_fails_sent_mismatch() { + let ctxi = MockInputOperations::send_input_context(); + ctxi.expect().returning(|_| 2); + + let ctxge = MockErrorOperations::get_last_error_context(); + ctxge.expect().returning(|| WIN32_ERROR(1)); + + send_input::(vec![build_unicode_input( + InputKeyPress::Up, + 0, + )]) + .unwrap(); + } } diff --git a/apps/desktop/desktop_native/autotype/src/windows/window_title.rs b/apps/desktop/desktop_native/autotype/src/windows/window_title.rs new file mode 100644 index 00000000000..58f06eb54c1 --- /dev/null +++ b/apps/desktop/desktop_native/autotype/src/windows/window_title.rs @@ -0,0 +1,298 @@ +use std::{ffi::OsString, os::windows::ffi::OsStringExt}; + +use anyhow::{anyhow, Result}; +use tracing::{debug, error, warn}; +use windows::Win32::{ + Foundation::HWND, + UI::WindowsAndMessaging::{GetForegroundWindow, GetWindowTextLengthW, GetWindowTextW}, +}; + +use super::{ErrorOperations, Win32ErrorOperations, WIN32_SUCCESS}; + +#[cfg_attr(test, mockall::automock)] +trait WindowHandleOperations { + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtextlengthw + fn get_window_text_length_w(&self) -> Result; + + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtextw + fn get_window_text_w(&self, buffer: &mut Vec) -> Result; +} + +/// `WindowHandle` provides a light wrapper over the `HWND` (which is just a void *). +/// The raw pointer can become invalid during runtime so it's validity must be checked +/// before usage. +struct WindowHandle { + handle: HWND, +} + +impl WindowHandle { + /// Create a new `WindowHandle` + fn new(handle: HWND) -> Self { + Self { handle } + } + + /// Assert that the raw pointer is valid. + fn validate(&self) -> Result<()> { + if self.handle.is_invalid() { + error!("Window handle is invalid."); + return Err(anyhow!("Window handle is invalid.")); + } + Ok(()) + } +} + +impl WindowHandleOperations for WindowHandle { + fn get_window_text_length_w(&self) -> Result { + self.validate()?; + let length = unsafe { GetWindowTextLengthW(self.handle) }; + Ok(length) + } + + fn get_window_text_w(&self, buffer: &mut Vec) -> Result { + self.validate()?; + let len_written = unsafe { GetWindowTextW(self.handle, buffer) }; + Ok(len_written) + } +} + +/// Gets the title bar string for the foreground window. +pub(super) fn get_foreground_window_title() -> Result { + let window_handle = get_foreground_window_handle()?; + + let expected_window_title_length = + get_window_title_length::(&window_handle)?; + + get_window_title::( + &window_handle, + expected_window_title_length, + ) +} + +/// Retrieves the foreground window handle and validates it. +fn get_foreground_window_handle() -> Result { + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getforegroundwindow + let handle = unsafe { GetForegroundWindow() }; + + debug!("GetForegroundWindow() called."); + + let window_handle = WindowHandle::new(handle); + window_handle.validate()?; + + Ok(window_handle) +} + +/// # Returns +/// +/// The length of the window title. +/// +/// # Errors +/// +/// - If the length zero and GetLastError() != 0, return the GetLastError() message. +fn get_window_title_length(window_handle: &H) -> Result +where + H: WindowHandleOperations, + E: ErrorOperations, +{ + // GetWindowTextLengthW does not itself clear the last error so we must do it ourselves. + E::set_last_error(0); + + let length = window_handle.get_window_text_length_w()?; + + let length = usize::try_from(length)?; + + debug!(length, "window text length retrieved from handle."); + + if length == 0 { + // attempt to retreive win32 error + let last_err = E::get_last_error(); + if last_err != WIN32_SUCCESS { + let last_err = last_err.to_hresult().message(); + error!(last_err, "Error getting window text length."); + return Err(anyhow!("Error getting window text length: {last_err}")); + } + } + + Ok(length) +} + +/// Gets the window title bar title using the expected length to determine size of buffer +/// to store it. +/// +/// # Returns +/// +/// If the `expected_title_length` is zero, return an Ok result containing empty string. It +/// Isn't considered an error by the Win32 API. +/// +/// Otherwise, return the retrieved window title string. +/// +/// # Errors +/// +/// - If the actual window title length (what the win32 API declares was written into the +/// buffer), is length zero and GetLastError() != 0 , return the GetLastError() message. +fn get_window_title(window_handle: &H, expected_title_length: usize) -> Result +where + H: WindowHandleOperations, + E: ErrorOperations, +{ + if expected_title_length == 0 { + // This isn't considered an error by the windows API, but in practice it means we can't + // match against the title so we'll stop here. + // The upstream will make a contains comparison on what we return, so an empty string + // will not result on a match. + warn!("Window title length is zero."); + return Ok(String::from("")); + } + + let mut buffer: Vec = vec![0; expected_title_length + 1]; // add extra space for the null character + + let actual_window_title_length = window_handle.get_window_text_w(&mut buffer)?; + + debug!(actual_window_title_length, "window title retrieved."); + + if actual_window_title_length == 0 { + // attempt to retreive win32 error + let last_err = E::get_last_error(); + if last_err != WIN32_SUCCESS { + let last_err = last_err.to_hresult().message(); + error!(last_err, "Error retrieving window title."); + return Err(anyhow!("Error retrieving window title: {last_err}")); + } + // in practice, we should not get to the below code, since we asserted the len > 0 + // above. but it is an extra protection in case the windows API didn't set an error. + warn!(expected_title_length, "No window title retrieved."); + } + + let window_title = OsString::from_wide(&buffer); + + Ok(window_title.to_string_lossy().into_owned()) +} + +#[cfg(test)] +mod tests { + //! For the mocking of the traits that are static methods, we need to use the `serial_test` crate + //! in order to mock those, since the mock expectations set have to be global in absence of a `self`. + //! More info: https://docs.rs/mockall/latest/mockall/#static-methods + + use super::*; + + use crate::windowing::MockErrorOperations; + use mockall::predicate; + use serial_test::serial; + use windows::Win32::Foundation::WIN32_ERROR; + + #[test] + #[serial] + fn get_window_title_length_can_be_zero() { + let mut mock_handle = MockWindowHandleOperations::new(); + + let ctxse = MockErrorOperations::set_last_error_context(); + ctxse + .expect() + .once() + .with(predicate::eq(0)) + .returning(|_| {}); + + mock_handle + .expect_get_window_text_length_w() + .once() + .returning(|| Ok(0)); + + let ctxge = MockErrorOperations::get_last_error_context(); + ctxge.expect().returning(|| WIN32_ERROR(0)); + + let len = get_window_title_length::( + &mock_handle, + ) + .unwrap(); + + assert_eq!(len, 0); + } + + #[test] + #[serial] + #[should_panic(expected = "Error getting window text length:")] + fn get_window_title_length_fails() { + let mut mock_handle = MockWindowHandleOperations::new(); + + let ctxse = MockErrorOperations::set_last_error_context(); + ctxse.expect().with(predicate::eq(0)).returning(|_| {}); + + mock_handle + .expect_get_window_text_length_w() + .once() + .returning(|| Ok(0)); + + let ctxge = MockErrorOperations::get_last_error_context(); + ctxge.expect().returning(|| WIN32_ERROR(1)); + + get_window_title_length::(&mock_handle) + .unwrap(); + } + + #[test] + fn get_window_title_succeeds() { + let mut mock_handle = MockWindowHandleOperations::new(); + + mock_handle + .expect_get_window_text_w() + .once() + .returning(|buffer| { + buffer.fill_with(|| 42); // because why not + Ok(42) + }); + + let title = + get_window_title::(&mock_handle, 42) + .unwrap(); + + assert_eq!(title.len(), 43); // That extra slot in the buffer for null char + + assert_eq!(title, "*******************************************"); + } + + #[test] + fn get_window_title_returns_empty_string() { + let mock_handle = MockWindowHandleOperations::new(); + + let title = + get_window_title::(&mock_handle, 0) + .unwrap(); + + assert_eq!(title, ""); + } + + #[test] + #[serial] + #[should_panic(expected = "Error retrieving window title:")] + fn get_window_title_fails_with_last_error() { + let mut mock_handle = MockWindowHandleOperations::new(); + + mock_handle + .expect_get_window_text_w() + .once() + .returning(|_| Ok(0)); + + let ctxge = MockErrorOperations::get_last_error_context(); + ctxge.expect().returning(|| WIN32_ERROR(1)); + + get_window_title::(&mock_handle, 42) + .unwrap(); + } + + #[test] + #[serial] + fn get_window_title_doesnt_fail_but_reads_zero() { + let mut mock_handle = MockWindowHandleOperations::new(); + + mock_handle + .expect_get_window_text_w() + .once() + .returning(|_| Ok(0)); + + let ctxge = MockErrorOperations::get_last_error_context(); + ctxge.expect().returning(|| WIN32_ERROR(0)); + + get_window_title::(&mock_handle, 42) + .unwrap(); + } +} From 81e9015b5b7ecbc38b50bbf3a0fb3420ea5c402d Mon Sep 17 00:00:00 2001 From: Tom <144813356+ttalty@users.noreply.github.com> Date: Thu, 23 Oct 2025 12:54:52 -0400 Subject: [PATCH 14/73] Adding include my items to the services and reports (#16987) --- .../exposed-passwords-report.component.ts | 2 +- .../reused-passwords-report.component.ts | 2 +- .../unsecured-websites-report.component.ts | 2 +- .../weak-passwords-report.component.ts | 2 +- libs/common/src/abstractions/api.service.ts | 5 ++++- libs/common/src/services/api.service.ts | 17 +++++++++-------- .../src/vault/abstractions/cipher.service.ts | 5 ++++- .../common/src/vault/services/cipher.service.ts | 10 ++++++++-- 8 files changed, 29 insertions(+), 16 deletions(-) diff --git a/apps/web/src/app/dirt/reports/pages/organizations/exposed-passwords-report.component.ts b/apps/web/src/app/dirt/reports/pages/organizations/exposed-passwords-report.component.ts index b88987e1d25..e7392ad609a 100644 --- a/apps/web/src/app/dirt/reports/pages/organizations/exposed-passwords-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/organizations/exposed-passwords-report.component.ts @@ -86,7 +86,7 @@ export class ExposedPasswordsReportComponent } getAllCiphers(): Promise { - return this.cipherService.getAllFromApiForOrganization(this.organization.id); + return this.cipherService.getAllFromApiForOrganization(this.organization.id, true); } canManageCipher(c: CipherView): boolean { diff --git a/apps/web/src/app/dirt/reports/pages/organizations/reused-passwords-report.component.ts b/apps/web/src/app/dirt/reports/pages/organizations/reused-passwords-report.component.ts index 7fcf3562437..5c48919510e 100644 --- a/apps/web/src/app/dirt/reports/pages/organizations/reused-passwords-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/organizations/reused-passwords-report.component.ts @@ -84,7 +84,7 @@ export class ReusedPasswordsReportComponent } getAllCiphers(): Promise { - return this.cipherService.getAllFromApiForOrganization(this.organization.id); + return this.cipherService.getAllFromApiForOrganization(this.organization.id, true); } canManageCipher(c: CipherView): boolean { diff --git a/apps/web/src/app/dirt/reports/pages/organizations/unsecured-websites-report.component.ts b/apps/web/src/app/dirt/reports/pages/organizations/unsecured-websites-report.component.ts index 2e916da0294..dad9688f105 100644 --- a/apps/web/src/app/dirt/reports/pages/organizations/unsecured-websites-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/organizations/unsecured-websites-report.component.ts @@ -89,7 +89,7 @@ export class UnsecuredWebsitesReportComponent } getAllCiphers(): Promise { - return this.cipherService.getAllFromApiForOrganization(this.organization.id); + return this.cipherService.getAllFromApiForOrganization(this.organization.id, true); } protected canManageCipher(c: CipherView): boolean { diff --git a/apps/web/src/app/dirt/reports/pages/organizations/weak-passwords-report.component.ts b/apps/web/src/app/dirt/reports/pages/organizations/weak-passwords-report.component.ts index 80be66e9ad2..67ca5081b6b 100644 --- a/apps/web/src/app/dirt/reports/pages/organizations/weak-passwords-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/organizations/weak-passwords-report.component.ts @@ -88,7 +88,7 @@ export class WeakPasswordsReportComponent } getAllCiphers(): Promise { - return this.cipherService.getAllFromApiForOrganization(this.organization.id); + return this.cipherService.getAllFromApiForOrganization(this.organization.id, true); } canManageCipher(c: CipherView): boolean { diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index 93e47a6d9a8..761038c2e46 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -194,7 +194,10 @@ export abstract class ApiService { cipherId: string, attachmentId: string, ): Promise; - abstract getCiphersOrganization(organizationId: string): Promise>; + abstract getCiphersOrganization( + organizationId: string, + includeMemberItems?: boolean, + ): Promise>; abstract postCipher(request: CipherRequest): Promise; abstract postCipherCreate(request: CipherCreateRequest): Promise; abstract postCipherAdmin(request: CipherCreateRequest): Promise; diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index 3b4fef9c5c4..b7f5f0ed001 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -408,14 +408,15 @@ export class ApiService implements ApiServiceAbstraction { return new CipherResponse(r); } - async getCiphersOrganization(organizationId: string): Promise> { - const r = await this.send( - "GET", - "/ciphers/organization-details?organizationId=" + organizationId, - null, - true, - true, - ); + async getCiphersOrganization( + organizationId: string, + includeMemberItems?: boolean, + ): Promise> { + let url = "/ciphers/organization-details?organizationId=" + organizationId; + if (includeMemberItems) { + url += `&includeMemberItems=${includeMemberItems}`; + } + const r = await this.send("GET", url, null, true, true); return new ListResponse(r, CipherResponse); } diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index 7971b6d4658..9aefd960b2f 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -76,7 +76,10 @@ export abstract class CipherService implements UserKeyRotationDataProvider; - abstract getAllFromApiForOrganization(organizationId: string): Promise; + abstract getAllFromApiForOrganization( + organizationId: string, + includeMemberItems?: boolean, + ): Promise; /** * Gets ciphers belonging to the specified organization that the user has explicit collection level access to. * Ciphers that are not assigned to any collections are only included for users with admin access. diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 8032c69ed7c..52c83c5a104 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -691,8 +691,14 @@ export class CipherService implements CipherServiceAbstraction { .sort((a, b) => this.sortCiphersByLastUsedThenName(a, b)); } - async getAllFromApiForOrganization(organizationId: string): Promise { - const response = await this.apiService.getCiphersOrganization(organizationId); + async getAllFromApiForOrganization( + organizationId: string, + includeMemberItems?: boolean, + ): Promise { + const response = await this.apiService.getCiphersOrganization( + organizationId, + includeMemberItems, + ); return await this.decryptOrganizationCiphersResponse(response, organizationId); } From 9b23b2d1b05aade55db6a5986f9194e23a6545c5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 23 Oct 2025 13:57:59 -0400 Subject: [PATCH 15/73] [deps]: Update uuid to v13 (#16636) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../native-messaging-test-runner/package-lock.json | 12 +++++++----- .../native-messaging-test-runner/package.json | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/desktop/native-messaging-test-runner/package-lock.json b/apps/desktop/native-messaging-test-runner/package-lock.json index 718bf7efb39..3b976891014 100644 --- a/apps/desktop/native-messaging-test-runner/package-lock.json +++ b/apps/desktop/native-messaging-test-runner/package-lock.json @@ -15,7 +15,7 @@ "@bitwarden/storage-core": "file:../../../libs/storage-core", "module-alias": "2.2.3", "ts-node": "10.9.2", - "uuid": "11.1.0", + "uuid": "13.0.0", "yargs": "18.0.0" }, "devDependencies": { @@ -121,6 +121,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.3.tgz", "integrity": "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -336,6 +337,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -351,16 +353,16 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], "license": "MIT", "bin": { - "uuid": "dist/esm/bin/uuid" + "uuid": "dist-node/bin/uuid" } }, "node_modules/v8-compile-cache-lib": { diff --git a/apps/desktop/native-messaging-test-runner/package.json b/apps/desktop/native-messaging-test-runner/package.json index 35a110c3958..0ca9cdc3a17 100644 --- a/apps/desktop/native-messaging-test-runner/package.json +++ b/apps/desktop/native-messaging-test-runner/package.json @@ -20,7 +20,7 @@ "@bitwarden/logging": "dist/libs/logging/src", "module-alias": "2.2.3", "ts-node": "10.9.2", - "uuid": "11.1.0", + "uuid": "13.0.0", "yargs": "18.0.0" }, "devDependencies": { From 2d34a19b23dc1497736ee6f1404b55a5c0844912 Mon Sep 17 00:00:00 2001 From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Date: Thu, 23 Oct 2025 13:41:38 -0500 Subject: [PATCH 16/73] [PM-25287] Add AddMasterPasswordUnlockData state migration (#16202) * Add AddMasterPasswordUnlockData state migration --- libs/state/src/state-migrations/migrate.ts | 6 +- ...73-add-master-password-unlock-data.spec.ts | 155 ++++++++++++++++++ .../73-add-master-password-unlock-data.ts | 72 ++++++++ 3 files changed, 231 insertions(+), 2 deletions(-) create mode 100644 libs/state/src/state-migrations/migrations/73-add-master-password-unlock-data.spec.ts create mode 100644 libs/state/src/state-migrations/migrations/73-add-master-password-unlock-data.ts diff --git a/libs/state/src/state-migrations/migrate.ts b/libs/state/src/state-migrations/migrate.ts index 620c2d3bb19..bf4cd17adba 100644 --- a/libs/state/src/state-migrations/migrate.ts +++ b/libs/state/src/state-migrations/migrate.ts @@ -69,12 +69,13 @@ import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric- import { RemoveAcBannersDismissed } from "./migrations/70-remove-ac-banner-dismissed"; import { RemoveNewCustomizationOptionsCalloutDismissed } from "./migrations/71-remove-new-customization-options-callout-dismissed"; import { RemoveAccountDeprovisioningBannerDismissed } from "./migrations/72-remove-account-deprovisioning-banner-dismissed"; +import { AddMasterPasswordUnlockData } from "./migrations/73-add-master-password-unlock-data"; import { MoveStateVersionMigrator } from "./migrations/8-move-state-version"; import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-settings-to-global"; import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 72; +export const CURRENT_VERSION = 73; export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -148,7 +149,8 @@ export function createMigrationBuilder() { .with(MigrateIncorrectFolderKey, 68, 69) .with(RemoveAcBannersDismissed, 69, 70) .with(RemoveNewCustomizationOptionsCalloutDismissed, 70, 71) - .with(RemoveAccountDeprovisioningBannerDismissed, 71, CURRENT_VERSION); + .with(RemoveAccountDeprovisioningBannerDismissed, 71, 72) + .with(AddMasterPasswordUnlockData, 72, CURRENT_VERSION); } export async function currentVersion( diff --git a/libs/state/src/state-migrations/migrations/73-add-master-password-unlock-data.spec.ts b/libs/state/src/state-migrations/migrations/73-add-master-password-unlock-data.spec.ts new file mode 100644 index 00000000000..28e65216653 --- /dev/null +++ b/libs/state/src/state-migrations/migrations/73-add-master-password-unlock-data.spec.ts @@ -0,0 +1,155 @@ +import { runMigrator } from "../migration-helper.spec"; + +import { AddMasterPasswordUnlockData } from "./73-add-master-password-unlock-data"; + +describe("AddMasterPasswordUnlockData", () => { + const sut = new AddMasterPasswordUnlockData(72, 73); + + describe("migrate", () => { + it("updates users that don't have master password unlock data", async () => { + const output = await runMigrator(sut, { + global_account_accounts: { + user1: { + email: "user1@email.Com", + name: "User 1", + }, + user2: { + email: "user2@email.com", + name: "User 2", + }, + }, + user_user1_masterPassword_masterKeyEncryptedUserKey: "user1MasterKeyEncryptedUser", + user_user1_kdfConfig_kdfConfig: { kdfType: 0, iterations: 600000 }, + user_user2_masterPassword_masterKeyEncryptedUserKey: "user2MasterKeyEncryptedUser", + user_user2_kdfConfig_kdfConfig: { kdfType: 0, iterations: 600001 }, + }); + + expect(output).toEqual({ + global_account_accounts: { + user1: { + email: "user1@email.Com", + name: "User 1", + }, + user2: { + email: "user2@email.com", + name: "User 2", + }, + }, + user_user1_masterPassword_masterKeyEncryptedUserKey: "user1MasterKeyEncryptedUser", + user_user1_kdfConfig_kdfConfig: { kdfType: 0, iterations: 600000 }, + user_user1_masterPasswordUnlock_masterPasswordUnlockKey: { + salt: "user1@email.com", + kdf: { kdfType: 0, iterations: 600000 }, + masterKeyWrappedUserKey: "user1MasterKeyEncryptedUser", + }, + user_user2_masterPassword_masterKeyEncryptedUserKey: "user2MasterKeyEncryptedUser", + user_user2_kdfConfig_kdfConfig: { kdfType: 0, iterations: 600001 }, + user_user2_masterPasswordUnlock_masterPasswordUnlockKey: { + salt: "user2@email.com", + kdf: { kdfType: 0, iterations: 600001 }, + masterKeyWrappedUserKey: "user2MasterKeyEncryptedUser", + }, + }); + }); + + it("does not update users that already have master password unlock data", async () => { + const output = await runMigrator(sut, { + global_account_accounts: { + user1: { + email: "user1@email.Com", + name: "User 1", + }, + }, + user_user1_masterPassword_masterKeyEncryptedUserKey: "user1MasterKeyEncryptedUser", + user_user1_kdfConfig_kdfConfig: { kdfType: 0, iterations: 600000 }, + user_user1_masterPasswordUnlock_masterPasswordUnlockKey: { someData: "data" }, + }); + + expect(output).toEqual({ + global_account_accounts: { + user1: { + email: "user1@email.Com", + name: "User 1", + }, + }, + user_user1_masterPassword_masterKeyEncryptedUserKey: "user1MasterKeyEncryptedUser", + user_user1_kdfConfig_kdfConfig: { kdfType: 0, iterations: 600000 }, + user_user1_masterPasswordUnlock_masterPasswordUnlockKey: { someData: "data" }, + }); + }); + + it("does not update users that have missing data required to construct master password unlock data", async () => { + const output = await runMigrator(sut, { + global_account_accounts: { + user1: { + name: "User 1", + }, + }, + user_user1_kdfConfig_kdfConfig: { kdfType: 0, iterations: 600000 }, + }); + + expect(output).toEqual({ + global_account_accounts: { + user1: { + name: "User 1", + }, + }, + user_user1_kdfConfig_kdfConfig: { kdfType: 0, iterations: 600000 }, + }); + }); + }); + + describe("rollback", () => { + it("rolls back data", async () => { + const output = await runMigrator( + sut, + { + global_account_accounts: { + user1: { + email: "user1@email.Com", + name: "User 1", + }, + user2: { + email: "user2@email.com", + name: "User 2", + }, + user3: { + email: "user3@email.com", + name: "User 3", + }, + }, + user_user1_masterPassword_masterKeyEncryptedUserKey: "user1MasterKeyEncryptedUser", + user_user1_kdfConfig_kdfConfig: { kdfType: 0, iterations: 600000 }, + user_user2_masterPassword_masterKeyEncryptedUserKey: "user2MasterKeyEncryptedUser", + user_user2_kdfConfig_kdfConfig: { kdfType: 0, iterations: 600001 }, + user_user1_masterPasswordUnlock_masterPasswordUnlockKey: "fakeData", + user_user2_masterPasswordUnlock_masterPasswordUnlockKey: "fakeData", + user_user3_masterPasswordUnlock_masterPasswordUnlockKey: null, + }, + "rollback", + ); + + expect(output).toEqual({ + global_account_accounts: { + user1: { + email: "user1@email.Com", + name: "User 1", + }, + user2: { + email: "user2@email.com", + name: "User 2", + }, + user3: { + email: "user3@email.com", + name: "User 3", + }, + }, + user_user1_masterPassword_masterKeyEncryptedUserKey: "user1MasterKeyEncryptedUser", + user_user1_kdfConfig_kdfConfig: { kdfType: 0, iterations: 600000 }, + user_user2_masterPassword_masterKeyEncryptedUserKey: "user2MasterKeyEncryptedUser", + user_user2_kdfConfig_kdfConfig: { kdfType: 0, iterations: 600001 }, + user_user3_masterPasswordUnlock_masterPasswordUnlockKey: null, + }); + }); + }); +}); diff --git a/libs/state/src/state-migrations/migrations/73-add-master-password-unlock-data.ts b/libs/state/src/state-migrations/migrations/73-add-master-password-unlock-data.ts new file mode 100644 index 00000000000..b9833f439a6 --- /dev/null +++ b/libs/state/src/state-migrations/migrations/73-add-master-password-unlock-data.ts @@ -0,0 +1,72 @@ +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { Migrator } from "../migrator"; + +export const ACCOUNT_ACCOUNTS: KeyDefinitionLike = { + stateDefinition: { + name: "account", + }, + key: "accounts", +}; + +export const MASTER_PASSWORD_UNLOCK_KEY: KeyDefinitionLike = { + key: "masterPasswordUnlockKey", + stateDefinition: { name: "masterPasswordUnlock" }, +}; + +export const MASTER_KEY_ENCRYPTED_USER_KEY: KeyDefinitionLike = { + key: "masterKeyEncryptedUserKey", + stateDefinition: { name: "masterPassword" }, +}; + +export const KDF_CONFIG_DISK: KeyDefinitionLike = { + key: "kdfConfig", + stateDefinition: { name: "kdfConfig" }, +}; + +type AccountsMap = Record; +type Account = { + email: string; + name: string; +}; + +export class AddMasterPasswordUnlockData extends Migrator<72, 73> { + async migrate(helper: MigrationHelper): Promise { + async function migrateAccount(userId: string, account: Account) { + const email = account.email; + const kdfConfig = await helper.getFromUser(userId, KDF_CONFIG_DISK); + const masterKeyEncryptedUserKey = await helper.getFromUser( + userId, + MASTER_KEY_ENCRYPTED_USER_KEY, + ); + if ( + (await helper.getFromUser(userId, MASTER_PASSWORD_UNLOCK_KEY)) == null && + email != null && + kdfConfig != null && + masterKeyEncryptedUserKey != null + ) { + await helper.setToUser(userId, MASTER_PASSWORD_UNLOCK_KEY, { + salt: email.trim().toLowerCase(), + kdf: kdfConfig, + masterKeyWrappedUserKey: masterKeyEncryptedUserKey, + }); + } + } + + const accountDictionary = await helper.getFromGlobal(ACCOUNT_ACCOUNTS); + const accounts = await helper.getAccounts(); + await Promise.all( + accounts.map(({ userId }) => migrateAccount(userId, accountDictionary[userId])), + ); + } + + async rollback(helper: MigrationHelper): Promise { + async function rollbackAccount(userId: string) { + if ((await helper.getFromUser(userId, MASTER_PASSWORD_UNLOCK_KEY)) != null) { + await helper.removeFromUser(userId, MASTER_PASSWORD_UNLOCK_KEY); + } + } + + const accounts = await helper.getAccounts(); + await Promise.all(accounts.map(({ userId }) => rollbackAccount(userId))); + } +} From d6785037ba902c7a748f81a8b550279ffad89fad Mon Sep 17 00:00:00 2001 From: Alex <55413326+AlexRubik@users.noreply.github.com> Date: Thu, 23 Oct 2025 14:53:39 -0400 Subject: [PATCH 17/73] PM-27254 Fix password change progress card reactivity (#16984) --- .../password-change-metric.component.ts | 97 +++++++++++-------- 1 file changed, 56 insertions(+), 41 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-cards/password-change-metric.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-cards/password-change-metric.component.ts index 910b326c662..941d693940b 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-cards/password-change-metric.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-cards/password-change-metric.component.ts @@ -1,7 +1,15 @@ import { CommonModule } from "@angular/common"; -import { Component, OnInit, ChangeDetectionStrategy } from "@angular/core"; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + DestroyRef, + OnInit, + inject, +} from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute } from "@angular/router"; -import { Subject, switchMap, takeUntil, of, BehaviorSubject, combineLatest } from "rxjs"; +import { switchMap, of, BehaviorSubject, combineLatest } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { @@ -26,6 +34,8 @@ import { AccessIntelligenceSecurityTasksService } from "../../shared/security-ta providers: [AccessIntelligenceSecurityTasksService, DefaultAdminTaskService], }) export class PasswordChangeMetricComponent implements OnInit { + private destroyRef = inject(DestroyRef); + protected taskMetrics$ = new BehaviorSubject({ totalTasks: 0, completedTasks: 0 }); private completedTasks: number = 0; private totalTasks: number = 0; @@ -34,14 +44,22 @@ export class PasswordChangeMetricComponent implements OnInit { atRiskAppsCount: number = 0; atRiskPasswordsCount: number = 0; private organizationId!: OrganizationId; - private destroyRef = new Subject(); renderMode: RenderMode = "noCriticalApps"; + // Computed properties (formerly getters) - updated when data changes + protected completedPercent = 0; + protected completedTasksCount = 0; + protected totalTasksCount = 0; + protected canAssignTasks = false; + protected hasExistingTasks = false; + protected newAtRiskPasswordsCount = 0; + constructor( private activatedRoute: ActivatedRoute, private securityTasksApiService: SecurityTasksApiService, private allActivitiesService: AllActivitiesService, protected accessIntelligenceSecurityTasksService: AccessIntelligenceSecurityTasksService, + private cdr: ChangeDetectorRef, ) {} async ngOnInit(): Promise { @@ -55,10 +73,11 @@ export class PasswordChangeMetricComponent implements OnInit { } return of({ totalTasks: 0, completedTasks: 0 }); }), - takeUntil(this.destroyRef), + takeUntilDestroyed(this.destroyRef), ) .subscribe((metrics) => { this.taskMetrics$.next(metrics); + this.cdr.markForCheck(); }); combineLatest([ @@ -67,7 +86,7 @@ export class PasswordChangeMetricComponent implements OnInit { this.allActivitiesService.atRiskPasswordsCount$, this.allActivitiesService.allApplicationsDetails$, ]) - .pipe(takeUntil(this.destroyRef)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(([taskMetrics, summary, atRiskPasswordsCount, allApplicationsDetails]) => { this.atRiskAppsCount = summary.totalCriticalAtRiskApplicationCount; this.atRiskPasswordsCount = atRiskPasswordsCount; @@ -81,6 +100,11 @@ export class PasswordChangeMetricComponent implements OnInit { this.allActivitiesService.setPasswordChangeProgressMetricHasProgressBar( this.renderMode === RenderMode.criticalAppsWithAtRiskAppsAndTasks, ); + + // Update all computed properties when data changes + this.updateComputedProperties(); + + this.cdr.markForCheck(); }); } @@ -116,57 +140,48 @@ export class PasswordChangeMetricComponent implements OnInit { return RenderMode.noCriticalApps; } - get completedPercent(): number { - if (this.totalTasks === 0) { - return 0; - } - return Math.round((this.completedTasks / this.totalTasks) * 100); - } + /** + * Updates all computed properties based on current state. + * Called whenever data changes to avoid recalculation on every change detection cycle. + */ + private updateComputedProperties(): void { + // Calculate completion percentage + this.completedPercent = + this.totalTasks === 0 ? 0 : Math.round((this.completedTasks / this.totalTasks) * 100); - get completedTasksCount(): number { + // Calculate completed tasks count based on render mode switch (this.renderMode) { case RenderMode.noCriticalApps: case RenderMode.criticalAppsWithAtRiskAppsAndNoTasks: - return 0; - + this.completedTasksCount = 0; + break; case RenderMode.criticalAppsWithAtRiskAppsAndTasks: - return this.completedTasks; - + this.completedTasksCount = this.completedTasks; + break; default: - return 0; + this.completedTasksCount = 0; } - } - get totalTasksCount(): number { + // Calculate total tasks count based on render mode switch (this.renderMode) { case RenderMode.noCriticalApps: - return 0; - + this.totalTasksCount = 0; + break; case RenderMode.criticalAppsWithAtRiskAppsAndNoTasks: - return this.atRiskAppsCount; - + this.totalTasksCount = this.atRiskAppsCount; + break; case RenderMode.criticalAppsWithAtRiskAppsAndTasks: - return this.totalTasks; - + this.totalTasksCount = this.totalTasks; + break; default: - return 0; + this.totalTasksCount = 0; } - } - get canAssignTasks(): boolean { - return this.atRiskPasswordsCount > this.totalTasks; - } - - get hasExistingTasks(): boolean { - return this.totalTasks > 0; - } - - get newAtRiskPasswordsCount(): number { - // Calculate new at-risk passwords as the difference between current count and tasks created - if (this.atRiskPasswordsCount > this.totalTasks) { - return this.atRiskPasswordsCount - this.totalTasks; - } - return 0; + // Calculate flags and counts + this.canAssignTasks = this.atRiskPasswordsCount > this.totalTasks; + this.hasExistingTasks = this.totalTasks > 0; + this.newAtRiskPasswordsCount = + this.atRiskPasswordsCount > this.totalTasks ? this.atRiskPasswordsCount - this.totalTasks : 0; } get renderModes() { From c80e8d1d8bf6a89dfdd015c281b54fbaac6a3210 Mon Sep 17 00:00:00 2001 From: Stephon Brown Date: Thu, 23 Oct 2025 15:05:50 -0400 Subject: [PATCH 18/73] [PM-27123] Account Credit not Showing for Premium Upgrade Payment (#16967) * fix(billing): Add NonTokenizedPaymentMethod type * fix(billing): Add NonTokenizedPayment type as parameter option * fix(billing): Update service for account credit payment and add tests * fix(billing): Add logic to accept account credit and callouts for credit * fix(billing): Add account credit back to premium component * fix(billing): update non-tokenizable payment method and payment service * refactor(billing): update payment component * fix(billing): update premium subscription request * fix(billing): update billing html component account credit logic --- .../billing/clients/account-billing.client.ts | 16 +- .../services/upgrade-payment.service.spec.ts | 151 +++++++++++++++-- .../services/upgrade-payment.service.ts | 42 ++++- .../upgrade-payment.component.html | 2 + .../upgrade-payment.component.ts | 155 ++++++++++++------ .../payment/types/tokenized-payment-method.ts | 6 + 6 files changed, 298 insertions(+), 74 deletions(-) diff --git a/apps/web/src/app/billing/clients/account-billing.client.ts b/apps/web/src/app/billing/clients/account-billing.client.ts index e5b97126fb3..256a06b3ead 100644 --- a/apps/web/src/app/billing/clients/account-billing.client.ts +++ b/apps/web/src/app/billing/clients/account-billing.client.ts @@ -2,7 +2,11 @@ import { Injectable } from "@angular/core"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { BillingAddress, TokenizedPaymentMethod } from "../payment/types"; +import { + BillingAddress, + NonTokenizedPaymentMethod, + TokenizedPaymentMethod, +} from "../payment/types"; @Injectable() export class AccountBillingClient { @@ -14,11 +18,17 @@ export class AccountBillingClient { } purchasePremiumSubscription = async ( - paymentMethod: TokenizedPaymentMethod, + paymentMethod: TokenizedPaymentMethod | NonTokenizedPaymentMethod, billingAddress: Pick, ): Promise => { const path = `${this.endpoint}/subscription`; - const request = { tokenizedPaymentMethod: paymentMethod, billingAddress: billingAddress }; + + // Determine the request payload based on the payment method type + const isTokenizedPayment = "token" in paymentMethod; + + const request = isTokenizedPayment + ? { tokenizedPaymentMethod: paymentMethod, billingAddress: billingAddress } + : { nonTokenizedPaymentMethod: paymentMethod, billingAddress: billingAddress }; await this.apiService.send("POST", path, request, true, true); }; } diff --git a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/services/upgrade-payment.service.spec.ts b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/services/upgrade-payment.service.spec.ts index 653a77dccdc..614fc862577 100644 --- a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/services/upgrade-payment.service.spec.ts +++ b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/services/upgrade-payment.service.spec.ts @@ -15,8 +15,18 @@ import { SyncService } from "@bitwarden/common/platform/sync"; import { UserId } from "@bitwarden/common/types/guid"; import { LogService } from "@bitwarden/logging"; -import { AccountBillingClient, TaxAmounts, TaxClient } from "../../../../clients"; -import { BillingAddress, TokenizedPaymentMethod } from "../../../../payment/types"; +import { + AccountBillingClient, + SubscriberBillingClient, + TaxAmounts, + TaxClient, +} from "../../../../clients"; +import { + BillingAddress, + NonTokenizablePaymentMethods, + NonTokenizedPaymentMethod, + TokenizedPaymentMethod, +} from "../../../../payment/types"; import { PersonalSubscriptionPricingTierIds } from "../../../../types/subscription-pricing-tier"; import { UpgradePaymentService, PlanDetails } from "./upgrade-payment.service"; @@ -30,6 +40,7 @@ describe("UpgradePaymentService", () => { const mockSyncService = mock(); const mockOrganizationService = mock(); const mockAccountService = mock(); + const mockSubscriberBillingClient = mock(); mockApiService.refreshIdentityToken.mockResolvedValue({}); mockSyncService.fullSync.mockResolvedValue(true); @@ -104,6 +115,7 @@ describe("UpgradePaymentService", () => { mockReset(mockLogService); mockReset(mockOrganizationService); mockReset(mockAccountService); + mockReset(mockSubscriberBillingClient); mockAccountService.activeAccount$ = of(null); mockOrganizationService.organizations$.mockReturnValue(of([])); @@ -111,7 +123,10 @@ describe("UpgradePaymentService", () => { TestBed.configureTestingModule({ providers: [ UpgradePaymentService, - + { + provide: SubscriberBillingClient, + useValue: mockSubscriberBillingClient, + }, { provide: OrganizationBillingServiceAbstraction, useValue: mockOrganizationBillingService, @@ -172,6 +187,7 @@ describe("UpgradePaymentService", () => { mockSyncService, mockOrganizationService, mockAccountService, + mockSubscriberBillingClient, ); // Act & Assert @@ -223,6 +239,7 @@ describe("UpgradePaymentService", () => { mockSyncService, mockOrganizationService, mockAccountService, + mockSubscriberBillingClient, ); // Act & Assert @@ -256,6 +273,7 @@ describe("UpgradePaymentService", () => { mockSyncService, mockOrganizationService, mockAccountService, + mockSubscriberBillingClient, ); // Act & Assert @@ -266,6 +284,68 @@ describe("UpgradePaymentService", () => { }); }); + describe("accountCredit$", () => { + it("should correctly fetch account credit for subscriber", (done) => { + // Arrange + + const mockAccount: Account = { + id: "user-id" as UserId, + email: "test@example.com", + name: "Test User", + emailVerified: true, + }; + const expectedCredit = 25.5; + + mockAccountService.activeAccount$ = of(mockAccount); + mockSubscriberBillingClient.getCredit.mockResolvedValue(expectedCredit); + + const service = new UpgradePaymentService( + mockOrganizationBillingService, + mockAccountBillingClient, + mockTaxClient, + mockLogService, + mockApiService, + mockSyncService, + mockOrganizationService, + mockAccountService, + mockSubscriberBillingClient, + ); + + // Act & Assert + service.accountCredit$.subscribe((credit) => { + expect(credit).toBe(expectedCredit); + expect(mockSubscriberBillingClient.getCredit).toHaveBeenCalledWith({ + data: mockAccount, + type: "account", + }); + done(); + }); + }); + + it("should handle empty account", (done) => { + // Arrange + mockAccountService.activeAccount$ = of(null); + const service = new UpgradePaymentService( + mockOrganizationBillingService, + mockAccountBillingClient, + mockTaxClient, + mockLogService, + mockApiService, + mockSyncService, + mockOrganizationService, + mockAccountService, + mockSubscriberBillingClient, + ); + // Act & Assert + service?.accountCredit$.subscribe({ + error: () => { + expect(mockSubscriberBillingClient.getCredit).not.toHaveBeenCalled(); + done(); + }, + }); + }); + }); + describe("adminConsoleRouteForOwnedOrganization$", () => { it("should return the admin console route for the first free organization the user owns", (done) => { // Arrange @@ -309,6 +389,7 @@ describe("UpgradePaymentService", () => { mockSyncService, mockOrganizationService, mockAccountService, + mockSubscriberBillingClient, ); // Act & Assert @@ -405,24 +486,58 @@ describe("UpgradePaymentService", () => { expect(mockSyncService.fullSync).toHaveBeenCalledWith(true); }); - it("should throw error if payment method is incomplete", async () => { + it("should handle upgrade with account credit payment method and refresh data", async () => { // Arrange - const incompletePaymentMethod = { type: "card" } as TokenizedPaymentMethod; + const accountCreditPaymentMethod: NonTokenizedPaymentMethod = { + type: NonTokenizablePaymentMethods.accountCredit, + }; + mockAccountBillingClient.purchasePremiumSubscription.mockResolvedValue(); - // Act & Assert - await expect( - sut.upgradeToPremium(incompletePaymentMethod, mockBillingAddress), - ).rejects.toThrow("Payment method type or token is missing"); + // Act + await sut.upgradeToPremium(accountCreditPaymentMethod, mockBillingAddress); + + // Assert + expect(mockAccountBillingClient.purchasePremiumSubscription).toHaveBeenCalledWith( + accountCreditPaymentMethod, + mockBillingAddress, + ); + expect(mockApiService.refreshIdentityToken).toHaveBeenCalled(); + expect(mockSyncService.fullSync).toHaveBeenCalledWith(true); }); - it("should throw error if billing address is incomplete", async () => { + it("should validate payment method type and token", async () => { // Arrange - const incompleteBillingAddress = { country: "US", postalCode: null } as any; + const noTypePaymentMethod = { token: "test-token" } as any; + const noTokenPaymentMethod = { type: "card" } as TokenizedPaymentMethod; + + // Act & Assert + await expect(sut.upgradeToPremium(noTypePaymentMethod, mockBillingAddress)).rejects.toThrow( + "Payment method type is missing", + ); + + await expect(sut.upgradeToPremium(noTokenPaymentMethod, mockBillingAddress)).rejects.toThrow( + "Payment method token is missing", + ); + }); + + it("should validate billing address fields", async () => { + // Arrange + const missingCountry = { postalCode: "12345" } as any; + const missingPostal = { country: "US" } as any; + const nullFields = { country: "US", postalCode: null } as any; // Act & Assert await expect( - sut.upgradeToPremium(mockTokenizedPaymentMethod, incompleteBillingAddress), + sut.upgradeToPremium(mockTokenizedPaymentMethod, missingCountry), ).rejects.toThrow("Billing address information is incomplete"); + + await expect(sut.upgradeToPremium(mockTokenizedPaymentMethod, missingPostal)).rejects.toThrow( + "Billing address information is incomplete", + ); + + await expect(sut.upgradeToPremium(mockTokenizedPaymentMethod, nullFields)).rejects.toThrow( + "Billing address information is incomplete", + ); }); }); @@ -504,7 +619,7 @@ describe("UpgradePaymentService", () => { expect(mockOrganizationBillingService.purchaseSubscription).toHaveBeenCalledTimes(1); }); - it("should throw error if payment method is incomplete", async () => { + it("should throw error if payment token is missing with card type", async () => { const incompletePaymentMethod = { type: "card" } as TokenizedPaymentMethod; await expect( @@ -512,7 +627,15 @@ describe("UpgradePaymentService", () => { organizationName: "Test Organization", billingAddress: mockBillingAddress, }), - ).rejects.toThrow("Payment method type or token is missing"); + ).rejects.toThrow("Payment method token is missing"); + }); + it("should throw error if organization name is missing", async () => { + await expect( + sut.upgradeToFamilies(mockAccount, mockFamiliesPlanDetails, mockTokenizedPaymentMethod, { + organizationName: "", + billingAddress: mockBillingAddress, + }), + ).rejects.toThrow("Organization name is required for families upgrade"); }); }); }); diff --git a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/services/upgrade-payment.service.ts b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/services/upgrade-payment.service.ts index 11dd10d4bb8..e175363af33 100644 --- a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/services/upgrade-payment.service.ts +++ b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/services/upgrade-payment.service.ts @@ -11,21 +11,25 @@ import { OrganizationBillingServiceAbstraction, SubscriptionInformation, } from "@bitwarden/common/billing/abstractions"; -import { PlanType } from "@bitwarden/common/billing/enums"; +import { PaymentMethodType, PlanType } from "@bitwarden/common/billing/enums"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { LogService } from "@bitwarden/logging"; import { AccountBillingClient, OrganizationSubscriptionPurchase, + SubscriberBillingClient, TaxAmounts, TaxClient, } from "../../../../clients"; import { BillingAddress, + NonTokenizablePaymentMethods, + NonTokenizedPaymentMethod, tokenizablePaymentMethodToLegacyEnum, TokenizedPaymentMethod, } from "../../../../payment/types"; +import { mapAccountToSubscriber } from "../../../../types"; import { PersonalSubscriptionPricingTier, PersonalSubscriptionPricingTierId, @@ -59,6 +63,7 @@ export class UpgradePaymentService { private syncService: SyncService, private organizationService: OrganizationService, private accountService: AccountService, + private subscriberBillingClient: SubscriberBillingClient, ) {} userIsOwnerOfFreeOrg$: Observable = this.accountService.activeAccount$.pipe( @@ -79,6 +84,12 @@ export class UpgradePaymentService { map((org) => `/organizations/${org!.id}/billing/subscription`), ); + // Fetch account credit + accountCredit$: Observable = this.accountService.activeAccount$.pipe( + mapAccountToSubscriber, + switchMap((account) => this.subscriberBillingClient.getCredit(account)), + ); + /** * Calculate estimated tax for the selected plan */ @@ -130,7 +141,7 @@ export class UpgradePaymentService { * Process premium upgrade */ async upgradeToPremium( - paymentMethod: TokenizedPaymentMethod, + paymentMethod: TokenizedPaymentMethod | NonTokenizedPaymentMethod, billingAddress: Pick, ): Promise { this.validatePaymentAndBillingInfo(paymentMethod, billingAddress); @@ -169,10 +180,7 @@ export class UpgradePaymentService { passwordManagerSeats: passwordManagerSeats, }, payment: { - paymentMethod: [ - paymentMethod.token, - tokenizablePaymentMethodToLegacyEnum(paymentMethod.type), - ], + paymentMethod: [paymentMethod.token, this.getPaymentMethodType(paymentMethod)], billing: { country: billingAddress.country, postalCode: billingAddress.postalCode, @@ -195,11 +203,19 @@ export class UpgradePaymentService { } private validatePaymentAndBillingInfo( - paymentMethod: TokenizedPaymentMethod, + paymentMethod: TokenizedPaymentMethod | NonTokenizedPaymentMethod, billingAddress: { country: string; postalCode: string }, ): void { - if (!paymentMethod?.token || !paymentMethod?.type) { - throw new Error("Payment method type or token is missing"); + if (!paymentMethod?.type) { + throw new Error("Payment method type is missing"); + } + + // Account credit does not require a token + if ( + paymentMethod.type !== NonTokenizablePaymentMethods.accountCredit && + !paymentMethod?.token + ) { + throw new Error("Payment method token is missing"); } if (!billingAddress?.country || !billingAddress?.postalCode) { @@ -211,4 +227,12 @@ export class UpgradePaymentService { await this.apiService.refreshIdentityToken(); await this.syncService.fullSync(true); } + + private getPaymentMethodType( + paymentMethod: TokenizedPaymentMethod | NonTokenizedPaymentMethod, + ): PaymentMethodType { + return paymentMethod.type === NonTokenizablePaymentMethods.accountCredit + ? PaymentMethodType.Credit + : tokenizablePaymentMethodToLegacyEnum(paymentMethod.type); + } } diff --git a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.html b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.html index fad883f942a..2228a6f6c06 100644 --- a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.html +++ b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.html @@ -34,8 +34,10 @@
{{ "paymentMethod" | i18n }}
{{ "billingAddress" | i18n }}
diff --git a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.ts b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.ts index 0b785d44e95..740e4eadff9 100644 --- a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.ts +++ b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.ts @@ -10,7 +10,16 @@ import { } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { catchError, debounceTime, from, Observable, of, switchMap } from "rxjs"; +import { + debounceTime, + Observable, + switchMap, + startWith, + from, + catchError, + of, + combineLatest, +} from "rxjs"; import { Account } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -23,7 +32,14 @@ import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { EnterBillingAddressComponent, EnterPaymentMethodComponent, + getBillingAddressFromForm, } from "../../../payment/components"; +import { + BillingAddress, + NonTokenizablePaymentMethods, + NonTokenizedPaymentMethod, + TokenizedPaymentMethod, +} from "../../../payment/types"; import { BillingServicesModule } from "../../../services"; import { SubscriptionPricingService } from "../../../services/subscription-pricing.service"; import { BitwardenSubscriber } from "../../../types"; @@ -33,7 +49,11 @@ import { PersonalSubscriptionPricingTierIds, } from "../../../types/subscription-pricing-tier"; -import { PlanDetails, UpgradePaymentService } from "./services/upgrade-payment.service"; +import { + PaymentFormValues, + PlanDetails, + UpgradePaymentService, +} from "./services/upgrade-payment.service"; /** * Status types for upgrade payment dialog @@ -80,6 +100,7 @@ export class UpgradePaymentComponent implements OnInit, AfterViewInit { protected goBack = output(); protected complete = output(); protected selectedPlan: PlanDetails | null = null; + protected hasEnoughAccountCredit$!: Observable; @ViewChild(EnterPaymentMethodComponent) paymentComponent!: EnterPaymentMethodComponent; @ViewChild(CartSummaryComponent) cartSummaryComponent!: CartSummaryComponent; @@ -155,6 +176,22 @@ export class UpgradePaymentComponent implements OnInit, AfterViewInit { .subscribe((tax) => { this.estimatedTax = tax; }); + + // Check if user has enough account credit for the purchase + this.hasEnoughAccountCredit$ = combineLatest([ + this.upgradePaymentService.accountCredit$, + this.formGroup.valueChanges.pipe(startWith(this.formGroup.value)), + ]).pipe( + switchMap(([credit, formValue]) => { + const selectedPaymentType = formValue.paymentForm?.type; + if (selectedPaymentType !== NonTokenizablePaymentMethods.accountCredit) { + return of(true); // Not using account credit, so this check doesn't apply + } + + return credit ? of(credit >= this.cartSummaryComponent.total()) : of(false); + }), + ); + this.loading.set(false); } @@ -210,76 +247,98 @@ export class UpgradePaymentComponent implements OnInit, AfterViewInit { } private async processUpgrade(): Promise { - // Get common values - const country = this.formGroup.value?.billingAddress?.country; - const postalCode = this.formGroup.value?.billingAddress?.postalCode; - if (!this.selectedPlan) { throw new Error("No plan selected"); } - if (!country || !postalCode) { + + const billingAddress = getBillingAddressFromForm(this.formGroup.controls.billingAddress); + const organizationName = this.formGroup.value?.organizationName; + + if (!billingAddress.country || !billingAddress.postalCode) { throw new Error("Billing address is incomplete"); } - // Validate organization name for Families plan - const organizationName = this.formGroup.value?.organizationName; if (this.isFamiliesPlan && !organizationName) { throw new Error("Organization name is required"); } - // Get payment method - const tokenizedPaymentMethod = await this.paymentComponent?.tokenize(); + const paymentMethod = await this.getPaymentMethod(); - if (!tokenizedPaymentMethod) { + if (!paymentMethod) { throw new Error("Payment method is required"); } - // Process the upgrade based on plan type - if (this.isFamiliesPlan) { - const paymentFormValues = { - organizationName, - billingAddress: { country, postalCode }, - }; + const isTokenizedPayment = "token" in paymentMethod; - const response = await this.upgradePaymentService.upgradeToFamilies( - this.account(), - this.selectedPlan, - tokenizedPaymentMethod, - paymentFormValues, - ); - - return { status: UpgradePaymentStatus.UpgradedToFamilies, organizationId: response.id }; - } else { - await this.upgradePaymentService.upgradeToPremium(tokenizedPaymentMethod, { - country, - postalCode, - }); - return { status: UpgradePaymentStatus.UpgradedToPremium, organizationId: null }; + if (!isTokenizedPayment && this.isFamiliesPlan) { + throw new Error("Tokenized payment is required for families plan"); } + + return this.isFamiliesPlan + ? this.processFamiliesUpgrade( + organizationName!, + billingAddress, + paymentMethod as TokenizedPaymentMethod, + ) + : this.processPremiumUpgrade(paymentMethod, billingAddress); + } + + private async processFamiliesUpgrade( + organizationName: string, + billingAddress: BillingAddress, + paymentMethod: TokenizedPaymentMethod, + ): Promise { + const paymentFormValues: PaymentFormValues = { + organizationName, + billingAddress, + }; + + const response = await this.upgradePaymentService.upgradeToFamilies( + this.account(), + this.selectedPlan!, + paymentMethod, + paymentFormValues, + ); + + return { status: UpgradePaymentStatus.UpgradedToFamilies, organizationId: response.id }; + } + + private async processPremiumUpgrade( + paymentMethod: NonTokenizedPaymentMethod | TokenizedPaymentMethod, + billingAddress: BillingAddress, + ): Promise { + await this.upgradePaymentService.upgradeToPremium(paymentMethod, billingAddress); + return { status: UpgradePaymentStatus.UpgradedToPremium, organizationId: null }; + } + + /** + * Get payment method based on selected type + * If using account credit, returns a non-tokenized payment method + * Otherwise, tokenizes the payment method from the payment component + */ + private async getPaymentMethod(): Promise< + NonTokenizedPaymentMethod | TokenizedPaymentMethod | null + > { + const isAccountCreditSelected = + this.formGroup.value?.paymentForm?.type === NonTokenizablePaymentMethods.accountCredit; + + if (isAccountCreditSelected) { + return { type: NonTokenizablePaymentMethods.accountCredit }; + } + + return await this.paymentComponent?.tokenize(); } // Create an observable for tax calculation private refreshSalesTax$(): Observable { - const billingAddress = { - country: this.formGroup.value?.billingAddress?.country, - postalCode: this.formGroup.value?.billingAddress?.postalCode, - }; - - if (!this.selectedPlan || !billingAddress.country || !billingAddress.postalCode) { + if (this.formGroup.invalid || !this.selectedPlan) { return of(0); } - // Convert Promise to Observable + const billingAddress = getBillingAddressFromForm(this.formGroup.controls.billingAddress); + return from( - this.upgradePaymentService.calculateEstimatedTax(this.selectedPlan, { - line1: null, - line2: null, - city: null, - state: null, - country: billingAddress.country, - postalCode: billingAddress.postalCode, - taxId: null, - }), + this.upgradePaymentService.calculateEstimatedTax(this.selectedPlan, billingAddress), ).pipe( catchError((error: unknown) => { this.logService.error("Tax calculation failed:", error); diff --git a/apps/web/src/app/billing/payment/types/tokenized-payment-method.ts b/apps/web/src/app/billing/payment/types/tokenized-payment-method.ts index 9b867329e66..d2cbfcf5101 100644 --- a/apps/web/src/app/billing/payment/types/tokenized-payment-method.ts +++ b/apps/web/src/app/billing/payment/types/tokenized-payment-method.ts @@ -17,6 +17,8 @@ export type AccountCreditPaymentMethod = typeof NonTokenizablePaymentMethods.acc export type TokenizablePaymentMethod = (typeof TokenizablePaymentMethods)[keyof typeof TokenizablePaymentMethods]; +export type NonTokenizablePaymentMethod = + (typeof NonTokenizablePaymentMethods)[keyof typeof NonTokenizablePaymentMethods]; export const isTokenizablePaymentMethod = (value: string): value is TokenizablePaymentMethod => { const valid = Object.values(TokenizablePaymentMethods) as readonly string[]; @@ -40,3 +42,7 @@ export type TokenizedPaymentMethod = { type: TokenizablePaymentMethod; token: string; }; + +export type NonTokenizedPaymentMethod = { + type: NonTokenizablePaymentMethod; +}; From a592f2b866f8f7fc3a905a27d9b1f2cd8225db97 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 23 Oct 2025 15:18:43 -0400 Subject: [PATCH 19/73] [deps]: Update actions/checkout action to v5 (#16424) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../workflows/alert-ddg-files-modified.yml | 2 +- .github/workflows/auto-branch-updater.yml | 2 +- .github/workflows/build-browser.yml | 12 +++++------ .github/workflows/build-cli.yml | 8 ++++---- .github/workflows/build-desktop.yml | 20 +++++++++---------- .github/workflows/build-web.yml | 8 ++++---- .github/workflows/chromatic.yml | 2 +- .github/workflows/crowdin-pull.yml | 2 +- .github/workflows/lint-crowdin-config.yml | 2 +- .github/workflows/lint.yml | 4 ++-- .github/workflows/locales-lint.yml | 4 ++-- .github/workflows/nx.yml | 2 +- .github/workflows/publish-cli.yml | 6 +++--- .github/workflows/publish-desktop.yml | 6 +++--- .github/workflows/publish-web.yml | 4 ++-- .github/workflows/release-browser.yml | 4 ++-- .github/workflows/release-cli.yml | 2 +- .github/workflows/release-desktop.yml | 2 +- .github/workflows/release-web.yml | 2 +- .github/workflows/repository-management.yml | 4 ++-- .../workflows/test-browser-interactions.yml | 2 +- .github/workflows/test.yml | 8 ++++---- .github/workflows/version-auto-bump.yml | 2 +- 23 files changed, 55 insertions(+), 55 deletions(-) diff --git a/.github/workflows/alert-ddg-files-modified.yml b/.github/workflows/alert-ddg-files-modified.yml index 84cd67ecd5b..4acab6b1c62 100644 --- a/.github/workflows/alert-ddg-files-modified.yml +++ b/.github/workflows/alert-ddg-files-modified.yml @@ -14,7 +14,7 @@ jobs: pull-requests: write steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 persist-credentials: false diff --git a/.github/workflows/auto-branch-updater.yml b/.github/workflows/auto-branch-updater.yml index ceebfb7e466..dcd031af0de 100644 --- a/.github/workflows/auto-branch-updater.yml +++ b/.github/workflows/auto-branch-updater.yml @@ -30,7 +30,7 @@ jobs: run: echo "branch=${GITHUB_REF#refs/heads/}" >> "$GITHUB_OUTPUT" - name: Checkout repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: ref: 'eu-web-${{ steps.setup.outputs.branch }}' fetch-depth: 0 diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index e3a49e414f9..5980ef507cc 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -55,7 +55,7 @@ jobs: has_secrets: ${{ steps.check-secrets.outputs.has_secrets }} steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false @@ -94,7 +94,7 @@ jobs: working-directory: apps/browser steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false @@ -146,7 +146,7 @@ jobs: _NODE_VERSION: ${{ needs.setup.outputs.node_version }} steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false @@ -248,7 +248,7 @@ jobs: artifact_name: "dist-opera-MV3" steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false @@ -360,7 +360,7 @@ jobs: _NODE_VERSION: ${{ needs.setup.outputs.node_version }} steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false @@ -511,7 +511,7 @@ jobs: - build-safari steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 839181c6107..1f7b35f3307 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -59,7 +59,7 @@ jobs: has_secrets: ${{ steps.check-secrets.outputs.has_secrets }} steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false @@ -114,7 +114,7 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false @@ -306,7 +306,7 @@ jobs: _WIN_PKG_VERSION: 3.5 steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false @@ -510,7 +510,7 @@ jobs: _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 51a0938552c..39549c4580c 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -55,7 +55,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false @@ -88,7 +88,7 @@ jobs: working-directory: apps/desktop steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: ref: ${{ github.event.pull_request.head.sha }} persist-credentials: true @@ -173,7 +173,7 @@ jobs: working-directory: apps/desktop steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false @@ -323,7 +323,7 @@ jobs: working-directory: apps/desktop steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false @@ -430,7 +430,7 @@ jobs: NODE_OPTIONS: --max_old_space_size=4096 steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false @@ -689,7 +689,7 @@ jobs: NODE_OPTIONS: --max_old_space_size=4096 steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: ref: ${{ github.event.pull_request.head.sha }} @@ -923,7 +923,7 @@ jobs: working-directory: apps/desktop steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false @@ -1150,7 +1150,7 @@ jobs: working-directory: apps/desktop steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false @@ -1411,7 +1411,7 @@ jobs: working-directory: apps/desktop steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false @@ -1737,7 +1737,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index 6733eeca1b4..ee7444f13a9 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -64,7 +64,7 @@ jobs: has_secrets: ${{ steps.check-secrets.outputs.has_secrets }} steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false @@ -135,7 +135,7 @@ jobs: _VERSION: ${{ needs.setup.outputs.version }} steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false @@ -165,7 +165,7 @@ jobs: echo "server_ref=$SERVER_REF" >> "$GITHUB_OUTPUT" - name: Check out Server repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: path: server repository: bitwarden/server @@ -357,7 +357,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index 133f5b730b8..ccac9cb32bb 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -31,7 +31,7 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 diff --git a/.github/workflows/crowdin-pull.yml b/.github/workflows/crowdin-pull.yml index 3be294145ec..f195afa86da 100644 --- a/.github/workflows/crowdin-pull.yml +++ b/.github/workflows/crowdin-pull.yml @@ -56,7 +56,7 @@ jobs: private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }} - name: Checkout repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: token: ${{ steps.app-token.outputs.token }} persist-credentials: false diff --git a/.github/workflows/lint-crowdin-config.yml b/.github/workflows/lint-crowdin-config.yml index 40f73f7fc5a..ee22a03963c 100644 --- a/.github/workflows/lint-crowdin-config.yml +++ b/.github/workflows/lint-crowdin-config.yml @@ -22,7 +22,7 @@ jobs: ] steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 0136bd2f70f..ae4f4f95aa6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false @@ -91,7 +91,7 @@ jobs: steps: - name: Checkout repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false diff --git a/.github/workflows/locales-lint.yml b/.github/workflows/locales-lint.yml index 26c910f955e..da79f9aa21f 100644 --- a/.github/workflows/locales-lint.yml +++ b/.github/workflows/locales-lint.yml @@ -17,11 +17,11 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false - name: Checkout base branch repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: ref: ${{ github.event.pull_request.base.sha }} path: base diff --git a/.github/workflows/nx.yml b/.github/workflows/nx.yml index 3e14169a065..43361bc983d 100644 --- a/.github/workflows/nx.yml +++ b/.github/workflows/nx.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 persist-credentials: false diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml index 9bbd982d32f..bcae79d077e 100644 --- a/.github/workflows/publish-cli.yml +++ b/.github/workflows/publish-cli.yml @@ -101,7 +101,7 @@ jobs: _PKG_VERSION: ${{ needs.setup.outputs.release_version }} steps: - name: Checkout repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false @@ -149,7 +149,7 @@ jobs: _PKG_VERSION: ${{ needs.setup.outputs.release_version }} steps: - name: Checkout repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false @@ -201,7 +201,7 @@ jobs: _PKG_VERSION: ${{ needs.setup.outputs.release_version }} steps: - name: Checkout repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false diff --git a/.github/workflows/publish-desktop.yml b/.github/workflows/publish-desktop.yml index a747012467e..2e9ba635e7a 100644 --- a/.github/workflows/publish-desktop.yml +++ b/.github/workflows/publish-desktop.yml @@ -221,7 +221,7 @@ jobs: _RELEASE_TAG: ${{ needs.setup.outputs.tag_name }} steps: - name: Checkout Repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false @@ -275,7 +275,7 @@ jobs: _RELEASE_TAG: ${{ needs.setup.outputs.tag_name }} steps: - name: Checkout Repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false @@ -332,7 +332,7 @@ jobs: _RELEASE_TAG: ${{ needs.setup.outputs.tag_name }} steps: - name: Checkout repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false diff --git a/.github/workflows/publish-web.yml b/.github/workflows/publish-web.yml index 9f9cbd5c58e..6bf2b282b38 100644 --- a/.github/workflows/publish-web.yml +++ b/.github/workflows/publish-web.yml @@ -28,7 +28,7 @@ jobs: contents: read steps: - name: Checkout repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false @@ -74,7 +74,7 @@ jobs: echo "Github Release Option: $_RELEASE_OPTION" - name: Checkout repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false diff --git a/.github/workflows/release-browser.yml b/.github/workflows/release-browser.yml index a2fda230491..39f54a6e2db 100644 --- a/.github/workflows/release-browser.yml +++ b/.github/workflows/release-browser.yml @@ -28,7 +28,7 @@ jobs: release_version: ${{ steps.version.outputs.version }} steps: - name: Checkout repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false @@ -61,7 +61,7 @@ jobs: contents: read steps: - name: Checkout repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 918f81e2723..d5013770476 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -29,7 +29,7 @@ jobs: release_version: ${{ steps.version.outputs.version }} steps: - name: Checkout repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index a97d72a32b0..9239914aeff 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -31,7 +31,7 @@ jobs: release_channel: ${{ steps.release_channel.outputs.channel }} steps: - name: Checkout repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false diff --git a/.github/workflows/release-web.yml b/.github/workflows/release-web.yml index d616d7adb3f..8c8f8ed86af 100644 --- a/.github/workflows/release-web.yml +++ b/.github/workflows/release-web.yml @@ -25,7 +25,7 @@ jobs: tag_version: ${{ steps.version.outputs.tag }} steps: - name: Checkout repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false diff --git a/.github/workflows/repository-management.yml b/.github/workflows/repository-management.yml index acfda4cdb11..ce9b70118b2 100644 --- a/.github/workflows/repository-management.yml +++ b/.github/workflows/repository-management.yml @@ -104,7 +104,7 @@ jobs: private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }} - name: Check out branch - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: ref: main token: ${{ steps.app-token.outputs.token }} @@ -469,7 +469,7 @@ jobs: private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }} - name: Check out target ref - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: ref: ${{ inputs.target_ref }} token: ${{ steps.app-token.outputs.token }} diff --git a/.github/workflows/test-browser-interactions.yml b/.github/workflows/test-browser-interactions.yml index a05f506d63f..a5b92563f5a 100644 --- a/.github/workflows/test-browser-interactions.yml +++ b/.github/workflows/test-browser-interactions.yml @@ -18,7 +18,7 @@ jobs: id-token: write steps: - name: Checkout code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 persist-credentials: false diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cf62df3180f..d468ca74ed6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false @@ -103,7 +103,7 @@ jobs: sudo apt-get install -y gnome-keyring dbus-x11 - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false @@ -137,7 +137,7 @@ jobs: runs-on: macos-14 steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false @@ -173,7 +173,7 @@ jobs: - rust-coverage steps: - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false diff --git a/.github/workflows/version-auto-bump.yml b/.github/workflows/version-auto-bump.yml index 0f7f2c9f46d..fee34d14e83 100644 --- a/.github/workflows/version-auto-bump.yml +++ b/.github/workflows/version-auto-bump.yml @@ -38,7 +38,7 @@ jobs: private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }} - name: Check out target ref - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: ref: main token: ${{ steps.app-token.outputs.token }} From e3f943364f519e4d3d5465b783100d35d3beeab7 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Thu, 23 Oct 2025 22:02:01 +0200 Subject: [PATCH 20/73] Billing - Prefer signal & change detection (#16944) --- .../popup/settings/premium-v2.component.ts | 2 ++ .../billing/app/accounts/premium.component.ts | 2 ++ .../billing-history-view.component.ts | 2 ++ .../account-payment-details.component.ts | 2 ++ .../premium/premium-vnext.component.ts | 2 ++ .../individual/premium/premium.component.ts | 4 +++ .../individual/subscription.component.ts | 2 ++ .../unified-upgrade-dialog.component.spec.ts | 4 +++ .../unified-upgrade-dialog.component.ts | 2 ++ .../upgrade-account.component.ts | 2 ++ .../upgrade-nav-button.component.ts | 2 ++ .../upgrade-payment.component.ts | 6 ++++ .../individual/user-subscription.component.ts | 2 ++ .../add-sponsorship-dialog.component.ts | 2 ++ .../free-bitwarden-families.component.ts | 4 ++- .../adjust-subscription.component.ts | 14 ++++++++ .../billing-sync-api-key.component.ts | 2 ++ .../billing-sync-key.component.ts | 2 ++ .../change-plan-dialog.component.ts | 24 ++++++++++++++ .../organizations/change-plan.component.ts | 12 +++++++ .../download-license.component.ts | 2 ++ ...nization-billing-history-view.component.ts | 2 ++ .../organization-plans.component.ts | 32 +++++++++++++++++++ ...ganization-subscription-cloud.component.ts | 2 ++ ...ization-subscription-selfhost.component.ts | 2 ++ .../organization-payment-details.component.ts | 2 ++ .../sm-adjust-subscription.component.ts | 8 +++++ .../sm-subscribe-standalone.component.ts | 10 ++++++ .../subscription-hidden.component.ts | 4 +++ .../subscription-status.component.ts | 6 ++++ ...ganization-free-trial-warning.component.ts | 8 +++++ ...tion-reseller-renewal-warning.component.ts | 4 +++ .../add-account-credit-dialog.component.ts | 4 +++ .../change-payment-method-dialog.component.ts | 2 ++ .../display-account-credit.component.ts | 6 ++++ .../display-billing-address.component.ts | 10 ++++++ .../display-payment-method.component.ts | 8 +++++ .../edit-billing-address-dialog.component.ts | 2 ++ .../enter-billing-address.component.ts | 6 ++++ .../enter-payment-method.component.ts | 14 ++++++++ .../components/payment-label.component.ts | 6 ++++ ...require-payment-method-dialog.component.ts | 2 ++ .../submit-payment-method-dialog.component.ts | 4 +++ .../verify-bank-account.component.ts | 6 ++++ .../settings/sponsored-families.component.ts | 2 ++ .../settings/sponsoring-org-row.component.ts | 8 +++++ .../adjust-storage-dialog.component.ts | 2 ++ ...illing-free-families-nav-item.component.ts | 2 ++ .../shared/billing-history.component.ts | 8 +++++ .../shared/offboarding-survey.component.ts | 2 ++ .../shared/plan-card/plan-card.component.ts | 4 ++- .../pricing-summary.component.ts | 4 +++ ...self-hosting-license-uploader.component.ts | 4 +++ ...self-hosting-license-uploader.component.ts | 4 +++ .../billing/shared/sm-subscribe.component.ts | 12 +++++++ .../trial-payment-dialog.component.ts | 8 ++++- .../shared/update-license-dialog.component.ts | 2 ++ .../shared/update-license.component.ts | 12 +++++++ .../complete-trial-initiation.component.ts | 4 +++ .../confirmation-details.component.ts | 10 ++++++ .../trial-billing-step.component.ts | 6 ++++ .../vertical-step-content.component.ts | 12 +++++++ .../vertical-step.component.ts | 8 +++++ .../vertical-stepper.component.ts | 4 +++ .../components/tax-id-warning.component.ts | 8 +++++ ...-existing-organization-dialog.component.ts | 2 ++ .../clients/create-client-dialog.component.ts | 2 ++ .../manage-client-name-dialog.component.ts | 2 ++ ...ge-client-subscription-dialog.component.ts | 2 ++ .../clients/manage-clients.component.ts | 2 ++ .../providers/clients/no-clients.component.ts | 8 +++++ .../free-families-sponsorship.component.ts | 2 ++ .../billing-history/invoices.component.ts | 10 ++++++ .../billing-history/no-invoices.component.ts | 2 ++ .../provider-billing-history.component.ts | 2 ++ .../provider-payment-details.component.ts | 2 ++ .../setup/setup-business-unit.component.ts | 2 ++ .../provider-subscription-status.component.ts | 4 +++ .../provider-subscription.component.ts | 2 ++ .../premium-badge/premium-badge.component.ts | 4 ++- .../cart-summary/cart-summary.component.ts | 12 ++++--- .../pricing-card.component.spec.ts | 2 ++ .../pricing-card/pricing-card.component.ts | 4 +++ 83 files changed, 429 insertions(+), 9 deletions(-) diff --git a/apps/browser/src/billing/popup/settings/premium-v2.component.ts b/apps/browser/src/billing/popup/settings/premium-v2.component.ts index fde44688349..b858b74242d 100644 --- a/apps/browser/src/billing/popup/settings/premium-v2.component.ts +++ b/apps/browser/src/billing/popup/settings/premium-v2.component.ts @@ -26,6 +26,8 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-premium", templateUrl: "premium-v2.component.html", diff --git a/apps/desktop/src/billing/app/accounts/premium.component.ts b/apps/desktop/src/billing/app/accounts/premium.component.ts index 5d0fa7a5dde..637969c1a21 100644 --- a/apps/desktop/src/billing/app/accounts/premium.component.ts +++ b/apps/desktop/src/billing/app/accounts/premium.component.ts @@ -10,6 +10,8 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService, ToastService } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-premium", templateUrl: "premium.component.html", diff --git a/apps/web/src/app/billing/individual/billing-history-view.component.ts b/apps/web/src/app/billing/individual/billing-history-view.component.ts index d615e01d0db..607a35baa94 100644 --- a/apps/web/src/app/billing/individual/billing-history-view.component.ts +++ b/apps/web/src/app/billing/individual/billing-history-view.component.ts @@ -10,6 +10,8 @@ import { } from "@bitwarden/common/billing/models/response/billing.response"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "billing-history-view.component.html", standalone: false, diff --git a/apps/web/src/app/billing/individual/payment-details/account-payment-details.component.ts b/apps/web/src/app/billing/individual/payment-details/account-payment-details.component.ts index ca7902542de..8c061894fac 100644 --- a/apps/web/src/app/billing/individual/payment-details/account-payment-details.component.ts +++ b/apps/web/src/app/billing/individual/payment-details/account-payment-details.component.ts @@ -19,6 +19,8 @@ type View = { credit: number | null; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "./account-payment-details.component.html", standalone: true, diff --git a/apps/web/src/app/billing/individual/premium/premium-vnext.component.ts b/apps/web/src/app/billing/individual/premium/premium-vnext.component.ts index 61994fdb61d..32c8061b10b 100644 --- a/apps/web/src/app/billing/individual/premium/premium-vnext.component.ts +++ b/apps/web/src/app/billing/individual/premium/premium-vnext.component.ts @@ -42,6 +42,8 @@ import { UnifiedUpgradeDialogStep, } from "../upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "./premium-vnext.component.html", standalone: true, diff --git a/apps/web/src/app/billing/individual/premium/premium.component.ts b/apps/web/src/app/billing/individual/premium/premium.component.ts index 526b020a9e3..6754f4c9f50 100644 --- a/apps/web/src/app/billing/individual/premium/premium.component.ts +++ b/apps/web/src/app/billing/individual/premium/premium.component.ts @@ -42,12 +42,16 @@ import { SubscriptionPricingService } from "@bitwarden/web-vault/app/billing/ser import { mapAccountToSubscriber } from "@bitwarden/web-vault/app/billing/types"; import { PersonalSubscriptionPricingTierIds } from "@bitwarden/web-vault/app/billing/types/subscription-pricing-tier"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "./premium.component.html", standalone: false, providers: [SubscriberBillingClient, TaxClient], }) export class PremiumComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild(EnterPaymentMethodComponent) enterPaymentMethodComponent!: EnterPaymentMethodComponent; protected hasPremiumFromAnyOrganization$: Observable; diff --git a/apps/web/src/app/billing/individual/subscription.component.ts b/apps/web/src/app/billing/individual/subscription.component.ts index 2a08ec85127..37fb2baf3a6 100644 --- a/apps/web/src/app/billing/individual/subscription.component.ts +++ b/apps/web/src/app/billing/individual/subscription.component.ts @@ -7,6 +7,8 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "subscription.component.html", standalone: false, diff --git a/apps/web/src/app/billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component.spec.ts b/apps/web/src/app/billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component.spec.ts index 1d707cec75f..d0960251724 100644 --- a/apps/web/src/app/billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component.spec.ts +++ b/apps/web/src/app/billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component.spec.ts @@ -26,6 +26,8 @@ import { UnifiedUpgradeDialogStep, } from "./unified-upgrade-dialog.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-upgrade-account", template: "", @@ -38,6 +40,8 @@ class MockUpgradeAccountComponent { closeClicked = output(); } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-upgrade-payment", template: "", diff --git a/apps/web/src/app/billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component.ts b/apps/web/src/app/billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component.ts index 0d9c8902d6c..077490cef43 100644 --- a/apps/web/src/app/billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component.ts +++ b/apps/web/src/app/billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component.ts @@ -62,6 +62,8 @@ export type UnifiedUpgradeDialogParams = { redirectOnCompletion?: boolean; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-unified-upgrade-dialog", imports: [ diff --git a/apps/web/src/app/billing/individual/upgrade/upgrade-account/upgrade-account.component.ts b/apps/web/src/app/billing/individual/upgrade/upgrade-account/upgrade-account.component.ts index c9b8f22d046..be09505d190 100644 --- a/apps/web/src/app/billing/individual/upgrade/upgrade-account/upgrade-account.component.ts +++ b/apps/web/src/app/billing/individual/upgrade/upgrade-account/upgrade-account.component.ts @@ -39,6 +39,8 @@ type CardDetails = { features: string[]; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-upgrade-account", imports: [ diff --git a/apps/web/src/app/billing/individual/upgrade/upgrade-nav-button/upgrade-nav-button/upgrade-nav-button.component.ts b/apps/web/src/app/billing/individual/upgrade/upgrade-nav-button/upgrade-nav-button/upgrade-nav-button.component.ts index 3d6f5b985ec..57d3b996e90 100644 --- a/apps/web/src/app/billing/individual/upgrade/upgrade-nav-button/upgrade-nav-button/upgrade-nav-button.component.ts +++ b/apps/web/src/app/billing/individual/upgrade/upgrade-nav-button/upgrade-nav-button/upgrade-nav-button.component.ts @@ -14,6 +14,8 @@ import { UnifiedUpgradeDialogStatus, } from "../../unified-upgrade-dialog/unified-upgrade-dialog.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-upgrade-nav-button", imports: [I18nPipe], diff --git a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.ts b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.ts index 740e4eadff9..5ad465455f2 100644 --- a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.ts +++ b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.ts @@ -80,6 +80,8 @@ export type UpgradePaymentParams = { subscriber: BitwardenSubscriber; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-upgrade-payment", imports: [ @@ -102,7 +104,11 @@ export class UpgradePaymentComponent implements OnInit, AfterViewInit { protected selectedPlan: PlanDetails | null = null; protected hasEnoughAccountCredit$!: Observable; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild(EnterPaymentMethodComponent) paymentComponent!: EnterPaymentMethodComponent; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild(CartSummaryComponent) cartSummaryComponent!: CartSummaryComponent; protected formGroup = new FormGroup({ diff --git a/apps/web/src/app/billing/individual/user-subscription.component.ts b/apps/web/src/app/billing/individual/user-subscription.component.ts index 4d1fa97785b..19db9ec8e61 100644 --- a/apps/web/src/app/billing/individual/user-subscription.component.ts +++ b/apps/web/src/app/billing/individual/user-subscription.component.ts @@ -26,6 +26,8 @@ import { import { UpdateLicenseDialogComponent } from "../shared/update-license-dialog.component"; import { UpdateLicenseDialogResult } from "../shared/update-license-types"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "user-subscription.component.html", standalone: false, diff --git a/apps/web/src/app/billing/members/add-sponsorship-dialog.component.ts b/apps/web/src/app/billing/members/add-sponsorship-dialog.component.ts index 38ae39cabfe..971cfb5704b 100644 --- a/apps/web/src/app/billing/members/add-sponsorship-dialog.component.ts +++ b/apps/web/src/app/billing/members/add-sponsorship-dialog.component.ts @@ -38,6 +38,8 @@ interface AddSponsorshipDialogParams { organizationKey: OrgKey; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "add-sponsorship-dialog.component.html", imports: [ diff --git a/apps/web/src/app/billing/members/free-bitwarden-families.component.ts b/apps/web/src/app/billing/members/free-bitwarden-families.component.ts index dc4a2f6df9b..474e513da6b 100644 --- a/apps/web/src/app/billing/members/free-bitwarden-families.component.ts +++ b/apps/web/src/app/billing/members/free-bitwarden-families.component.ts @@ -20,13 +20,15 @@ import { KeyService } from "@bitwarden/key-management"; import { AddSponsorshipDialogComponent } from "./add-sponsorship-dialog.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-free-bitwarden-families", templateUrl: "free-bitwarden-families.component.html", standalone: false, }) export class FreeBitwardenFamiliesComponent implements OnInit { - loading = signal(true); + readonly loading = signal(true); tabIndex = 0; sponsoredFamilies: OrganizationSponsorshipInvitesResponse[] = []; diff --git a/apps/web/src/app/billing/organizations/adjust-subscription.component.ts b/apps/web/src/app/billing/organizations/adjust-subscription.component.ts index d1086a6646b..7ee5891e8a9 100644 --- a/apps/web/src/app/billing/organizations/adjust-subscription.component.ts +++ b/apps/web/src/app/billing/organizations/adjust-subscription.component.ts @@ -16,17 +16,31 @@ import { OrganizationSubscriptionUpdateRequest } from "@bitwarden/common/billing import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ToastService } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-adjust-subscription", templateUrl: "adjust-subscription.component.html", standalone: false, }) export class AdjustSubscription implements OnInit, OnDestroy { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() organizationId: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() maxAutoscaleSeats: number; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() currentSeatCount: number; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() seatPrice = 0; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() interval = "year"; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onAdjusted = new EventEmitter(); private destroy$ = new Subject(); diff --git a/apps/web/src/app/billing/organizations/billing-sync-api-key.component.ts b/apps/web/src/app/billing/organizations/billing-sync-api-key.component.ts index 55687f00052..52a7fab60f5 100644 --- a/apps/web/src/app/billing/organizations/billing-sync-api-key.component.ts +++ b/apps/web/src/app/billing/organizations/billing-sync-api-key.component.ts @@ -20,6 +20,8 @@ export interface BillingSyncApiModalData { hasBillingToken: boolean; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "billing-sync-api-key.component.html", standalone: false, diff --git a/apps/web/src/app/billing/organizations/billing-sync-key.component.ts b/apps/web/src/app/billing/organizations/billing-sync-key.component.ts index 37ebefc803a..c6c2bf379eb 100644 --- a/apps/web/src/app/billing/organizations/billing-sync-key.component.ts +++ b/apps/web/src/app/billing/organizations/billing-sync-key.component.ts @@ -19,6 +19,8 @@ export interface BillingSyncKeyModalData { setParentConnection: (connection: OrganizationConnectionResponse) => void; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "billing-sync-key.component.html", standalone: false, diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts index c2c819ddf4d..ac415ac4be2 100644 --- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts @@ -105,6 +105,8 @@ interface OnSuccessArgs { organizationId: string; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "./change-plan-dialog.component.html", imports: [ @@ -116,13 +118,25 @@ interface OnSuccessArgs { providers: [SubscriberBillingClient, TaxClient], }) export class ChangePlanDialogComponent implements OnInit, OnDestroy { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild(EnterPaymentMethodComponent) enterPaymentMethodComponent: EnterPaymentMethodComponent; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() acceptingSponsorship = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() organizationId: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() showFree = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() showCancel = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() get productTier(): ProductTierType { return this._productTier; @@ -136,6 +150,8 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { protected estimatedTax: number = 0; private _productTier = ProductTierType.Free; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() get plan(): PlanType { return this._plan; @@ -147,9 +163,17 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } private _plan = PlanType.Free; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() providerId?: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onSuccess = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onCanceled = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onTrialBillingSuccess = new EventEmitter(); protected discountPercentageFromSub: number; diff --git a/apps/web/src/app/billing/organizations/change-plan.component.ts b/apps/web/src/app/billing/organizations/change-plan.component.ts index 31cbf4e94bf..a3f14f5ce29 100644 --- a/apps/web/src/app/billing/organizations/change-plan.component.ts +++ b/apps/web/src/app/billing/organizations/change-plan.component.ts @@ -6,16 +6,28 @@ import { ProductTierType } from "@bitwarden/common/billing/enums"; import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-change-plan", templateUrl: "change-plan.component.html", standalone: false, }) export class ChangePlanComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() organizationId: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() currentPlan: PlanResponse; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() preSelectedProductTier: ProductTierType; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onChanged = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onCanceled = new EventEmitter(); formPromise: Promise; diff --git a/apps/web/src/app/billing/organizations/download-license.component.ts b/apps/web/src/app/billing/organizations/download-license.component.ts index 8ada57e8377..e93ae5028dc 100644 --- a/apps/web/src/app/billing/organizations/download-license.component.ts +++ b/apps/web/src/app/billing/organizations/download-license.component.ts @@ -18,6 +18,8 @@ type DownloadLicenseDialogData = { organizationId: string; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "download-license.component.html", standalone: false, diff --git a/apps/web/src/app/billing/organizations/organization-billing-history-view.component.ts b/apps/web/src/app/billing/organizations/organization-billing-history-view.component.ts index ce4678ad8ef..a654ac272fe 100644 --- a/apps/web/src/app/billing/organizations/organization-billing-history-view.component.ts +++ b/apps/web/src/app/billing/organizations/organization-billing-history-view.component.ts @@ -10,6 +10,8 @@ import { BillingTransactionResponse, } from "@bitwarden/common/billing/models/response/billing.response"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "organization-billing-history-view.component.html", standalone: false, diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.ts b/apps/web/src/app/billing/organizations/organization-plans.component.ts index cbeedc454dc..a4ebba7a760 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.ts +++ b/apps/web/src/app/billing/organizations/organization-plans.component.ts @@ -72,6 +72,8 @@ const Allowed2020PlansForLegacyProviders = [ PlanType.EnterpriseMonthly2020, ]; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-organization-plans", templateUrl: "organization-plans.component.html", @@ -84,17 +86,33 @@ const Allowed2020PlansForLegacyProviders = [ providers: [SubscriberBillingClient, TaxClient], }) export class OrganizationPlansComponent implements OnInit, OnDestroy { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild(EnterPaymentMethodComponent) enterPaymentMethodComponent!: EnterPaymentMethodComponent; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() organizationId?: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() showFree = true; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() showCancel = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() acceptingSponsorship = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() planSponsorshipType?: PlanSponsorshipType; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() currentPlan: PlanResponse; selectedFile: File; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() get productTier(): ProductTierType { return this._productTier; @@ -107,6 +125,8 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { private _productTier = ProductTierType.Free; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() get plan(): PlanType { return this._plan; @@ -116,13 +136,25 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { this._plan = plan; this.formGroup?.controls?.plan?.setValue(plan); } + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() enableSecretsManagerByDefault: boolean; private _plan = PlanType.Free; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() providerId?: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() preSelectedProductTier?: ProductTierType; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onSuccess = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onCanceled = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onTrialBillingSuccess = new EventEmitter(); loading = true; diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts index 79d4057fdd7..fc9f8b1d986 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts @@ -42,6 +42,8 @@ import { ChangePlanDialogResultType, openChangePlanDialog } from "./change-plan- import { DownloadLicenceDialogComponent } from "./download-license.component"; import { SecretsManagerSubscriptionOptions } from "./sm-adjust-subscription.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "organization-subscription-cloud.component.html", standalone: false, diff --git a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts index fa4b633cb7a..905e682ceca 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts @@ -34,6 +34,8 @@ enum LicenseOptions { UPLOAD = 1, } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "organization-subscription-selfhost.component.html", standalone: false, diff --git a/apps/web/src/app/billing/organizations/payment-details/organization-payment-details.component.ts b/apps/web/src/app/billing/organizations/payment-details/organization-payment-details.component.ts index b2bf27e726a..9609160089b 100644 --- a/apps/web/src/app/billing/organizations/payment-details/organization-payment-details.component.ts +++ b/apps/web/src/app/billing/organizations/payment-details/organization-payment-details.component.ts @@ -60,6 +60,8 @@ const BANK_ACCOUNT_VERIFIED_COMMAND = new CommandDefinition<{ organizationId: st "organizationBankAccountVerified", ); +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "./organization-payment-details.component.html", standalone: true, diff --git a/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.ts b/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.ts index 33413832865..5fa6971bac6 100644 --- a/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.ts +++ b/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.ts @@ -56,14 +56,22 @@ export interface SecretsManagerSubscriptionOptions { additionalServiceAccountPrice: number; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-sm-adjust-subscription", templateUrl: "sm-adjust-subscription.component.html", standalone: false, }) export class SecretsManagerAdjustSubscriptionComponent implements OnInit, OnDestroy { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() organizationId: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() options: SecretsManagerSubscriptionOptions; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onAdjusted = new EventEmitter(); private destroy$ = new Subject(); diff --git a/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.ts b/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.ts index 6f9525e4fce..1ef705fd4bd 100644 --- a/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.ts +++ b/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.ts @@ -20,15 +20,25 @@ import { ToastService } from "@bitwarden/components"; import { secretsManagerSubscribeFormFactory } from "../shared"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "sm-subscribe-standalone", templateUrl: "sm-subscribe-standalone.component.html", standalone: false, }) export class SecretsManagerSubscribeStandaloneComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() plan: PlanResponse; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() organization: Organization; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() customerDiscount: BillingCustomerDiscount; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onSubscribe = new EventEmitter(); formGroup = secretsManagerSubscribeFormFactory(this.formBuilder); diff --git a/apps/web/src/app/billing/organizations/subscription-hidden.component.ts b/apps/web/src/app/billing/organizations/subscription-hidden.component.ts index cca12e938d2..d56167d6d70 100644 --- a/apps/web/src/app/billing/organizations/subscription-hidden.component.ts +++ b/apps/web/src/app/billing/organizations/subscription-hidden.component.ts @@ -4,6 +4,8 @@ import { Component, Input } from "@angular/core"; import { GearIcon } from "@bitwarden/assets/svg"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-org-subscription-hidden", template: `
@@ -16,6 +18,8 @@ import { GearIcon } from "@bitwarden/assets/svg"; standalone: false, }) export class SubscriptionHiddenComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() providerName: string; gearIcon = GearIcon; } diff --git a/apps/web/src/app/billing/organizations/subscription-status.component.ts b/apps/web/src/app/billing/organizations/subscription-status.component.ts index 0b59df3f707..54a309a441b 100644 --- a/apps/web/src/app/billing/organizations/subscription-status.component.ts +++ b/apps/web/src/app/billing/organizations/subscription-status.component.ts @@ -23,13 +23,19 @@ type ComponentData = { }; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-subscription-status", templateUrl: "subscription-status.component.html", standalone: false, }) export class SubscriptionStatusComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) organizationSubscriptionResponse: OrganizationSubscriptionResponse; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() reinstatementRequested = new EventEmitter(); constructor( diff --git a/apps/web/src/app/billing/organizations/warnings/components/organization-free-trial-warning.component.ts b/apps/web/src/app/billing/organizations/warnings/components/organization-free-trial-warning.component.ts index 8390e432236..debac3cb2f7 100644 --- a/apps/web/src/app/billing/organizations/warnings/components/organization-free-trial-warning.component.ts +++ b/apps/web/src/app/billing/organizations/warnings/components/organization-free-trial-warning.component.ts @@ -8,6 +8,8 @@ import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { OrganizationWarningsService } from "../services"; import { OrganizationFreeTrialWarning } from "../types"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-organization-free-trial-warning", template: ` @@ -36,8 +38,14 @@ import { OrganizationFreeTrialWarning } from "../types"; imports: [BannerModule, SharedModule], }) export class OrganizationFreeTrialWarningComponent implements OnInit { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) organization!: Organization; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() includeOrganizationNameInMessaging = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() clicked = new EventEmitter(); warning$!: Observable; diff --git a/apps/web/src/app/billing/organizations/warnings/components/organization-reseller-renewal-warning.component.ts b/apps/web/src/app/billing/organizations/warnings/components/organization-reseller-renewal-warning.component.ts index c49f59f6b05..e9850b55c9e 100644 --- a/apps/web/src/app/billing/organizations/warnings/components/organization-reseller-renewal-warning.component.ts +++ b/apps/web/src/app/billing/organizations/warnings/components/organization-reseller-renewal-warning.component.ts @@ -8,6 +8,8 @@ import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { OrganizationWarningsService } from "../services"; import { OrganizationResellerRenewalWarning } from "../types"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-organization-reseller-renewal-warning", template: ` @@ -27,6 +29,8 @@ import { OrganizationResellerRenewalWarning } from "../types"; imports: [BannerModule, SharedModule], }) export class OrganizationResellerRenewalWarningComponent implements OnInit { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) organization!: Organization; warning$!: Observable; diff --git a/apps/web/src/app/billing/payment/components/add-account-credit-dialog.component.ts b/apps/web/src/app/billing/payment/components/add-account-credit-dialog.component.ts index a83a00e8158..1bc08159cdf 100644 --- a/apps/web/src/app/billing/payment/components/add-account-credit-dialog.component.ts +++ b/apps/web/src/app/billing/payment/components/add-account-credit-dialog.component.ts @@ -52,6 +52,8 @@ const positiveNumberValidator = return null; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: `
@@ -128,6 +130,8 @@ const positiveNumberValidator = providers: [SubscriberBillingClient], }) export class AddAccountCreditDialogComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("payPalForm", { read: ElementRef, static: true }) payPalForm!: ElementRef; protected payPalConfig = process.env.PAYPAL_CONFIG as PayPalConfig; diff --git a/apps/web/src/app/billing/payment/components/change-payment-method-dialog.component.ts b/apps/web/src/app/billing/payment/components/change-payment-method-dialog.component.ts index 4d2fadaa894..71d156ecb26 100644 --- a/apps/web/src/app/billing/payment/components/change-payment-method-dialog.component.ts +++ b/apps/web/src/app/billing/payment/components/change-payment-method-dialog.component.ts @@ -18,6 +18,8 @@ type DialogParams = { subscriber: BitwardenSubscriber; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: ` diff --git a/apps/web/src/app/billing/payment/components/display-account-credit.component.ts b/apps/web/src/app/billing/payment/components/display-account-credit.component.ts index f6aa0ef58bb..b4684f0d739 100644 --- a/apps/web/src/app/billing/payment/components/display-account-credit.component.ts +++ b/apps/web/src/app/billing/payment/components/display-account-credit.component.ts @@ -10,6 +10,8 @@ import { BitwardenSubscriber } from "../../types"; import { AddAccountCreditDialogComponent } from "./add-account-credit-dialog.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-display-account-credit", template: ` @@ -26,7 +28,11 @@ import { AddAccountCreditDialogComponent } from "./add-account-credit-dialog.com providers: [SubscriberBillingClient, CurrencyPipe], }) export class DisplayAccountCreditComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) subscriber!: BitwardenSubscriber; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) credit!: number | null; constructor( diff --git a/apps/web/src/app/billing/payment/components/display-billing-address.component.ts b/apps/web/src/app/billing/payment/components/display-billing-address.component.ts index 03d21a79003..2c5b7986c7b 100644 --- a/apps/web/src/app/billing/payment/components/display-billing-address.component.ts +++ b/apps/web/src/app/billing/payment/components/display-billing-address.component.ts @@ -12,6 +12,8 @@ import { } from "@bitwarden/web-vault/app/billing/warnings/types"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-display-billing-address", template: ` @@ -48,9 +50,17 @@ import { SharedModule } from "@bitwarden/web-vault/app/shared"; imports: [AddressPipe, SharedModule], }) export class DisplayBillingAddressComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) subscriber!: BitwardenSubscriber; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) billingAddress!: BillingAddress | null; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() taxIdWarning?: TaxIdWarningType; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() updated = new EventEmitter(); constructor(private dialogService: DialogService) {} diff --git a/apps/web/src/app/billing/payment/components/display-payment-method.component.ts b/apps/web/src/app/billing/payment/components/display-payment-method.component.ts index 5f5e3442935..c5ffa4268ed 100644 --- a/apps/web/src/app/billing/payment/components/display-payment-method.component.ts +++ b/apps/web/src/app/billing/payment/components/display-payment-method.component.ts @@ -9,6 +9,8 @@ import { getCardBrandIcon, MaskedPaymentMethod } from "../types"; import { ChangePaymentMethodDialogComponent } from "./change-payment-method-dialog.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-display-payment-method", template: ` @@ -70,8 +72,14 @@ import { ChangePaymentMethodDialogComponent } from "./change-payment-method-dial imports: [SharedModule], }) export class DisplayPaymentMethodComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) subscriber!: BitwardenSubscriber; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) paymentMethod!: MaskedPaymentMethod | null; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() updated = new EventEmitter(); constructor(private dialogService: DialogService) {} diff --git a/apps/web/src/app/billing/payment/components/edit-billing-address-dialog.component.ts b/apps/web/src/app/billing/payment/components/edit-billing-address-dialog.component.ts index 6e356097d32..aa9d2830527 100644 --- a/apps/web/src/app/billing/payment/components/edit-billing-address-dialog.component.ts +++ b/apps/web/src/app/billing/payment/components/edit-billing-address-dialog.component.ts @@ -35,6 +35,8 @@ type DialogResult = | { type: "error" } | { type: "success"; billingAddress: BillingAddress }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: ` diff --git a/apps/web/src/app/billing/payment/components/enter-billing-address.component.ts b/apps/web/src/app/billing/payment/components/enter-billing-address.component.ts index 3f68c12c897..40785e9b7ea 100644 --- a/apps/web/src/app/billing/payment/components/enter-billing-address.component.ts +++ b/apps/web/src/app/billing/payment/components/enter-billing-address.component.ts @@ -47,6 +47,8 @@ type Scenario = taxIdWarning?: TaxIdWarningType; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-enter-billing-address", template: ` @@ -159,7 +161,11 @@ type Scenario = imports: [SharedModule], }) export class EnterBillingAddressComponent implements OnInit, OnDestroy { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) scenario!: Scenario; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) group!: BillingAddressFormGroup; protected selectableCountries = selectableCountries; diff --git a/apps/web/src/app/billing/payment/components/enter-payment-method.component.ts b/apps/web/src/app/billing/payment/components/enter-payment-method.component.ts index c0a9027388d..b75a4acb602 100644 --- a/apps/web/src/app/billing/payment/components/enter-payment-method.component.ts +++ b/apps/web/src/app/billing/payment/components/enter-payment-method.component.ts @@ -34,6 +34,8 @@ type PaymentMethodFormGroup = FormGroup<{ }>; }>; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-enter-payment-method", template: ` @@ -232,12 +234,24 @@ type PaymentMethodFormGroup = FormGroup<{ imports: [BillingServicesModule, PaymentLabelComponent, PopoverModule, SharedModule], }) export class EnterPaymentMethodComponent implements OnInit { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) group!: PaymentMethodFormGroup; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() private showBankAccount = true; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() showPayPal = true; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() showAccountCredit = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() hasEnoughAccountCredit = true; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() includeBillingAddress = false; protected showBankAccount$!: Observable; diff --git a/apps/web/src/app/billing/payment/components/payment-label.component.ts b/apps/web/src/app/billing/payment/components/payment-label.component.ts index 8ecc7b7fd9e..5842235679c 100644 --- a/apps/web/src/app/billing/payment/components/payment-label.component.ts +++ b/apps/web/src/app/billing/payment/components/payment-label.component.ts @@ -11,6 +11,8 @@ import { SharedModule } from "../../../shared"; * * Applies the same label styles from CL form-field component */ +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-payment-label", template: ` @@ -32,8 +34,12 @@ import { SharedModule } from "../../../shared"; }) export class PaymentLabelComponent { /** `id` of the associated input */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) for: string; /** Displays required text on the label */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ transform: booleanAttribute }) required = false; constructor() {} diff --git a/apps/web/src/app/billing/payment/components/require-payment-method-dialog.component.ts b/apps/web/src/app/billing/payment/components/require-payment-method-dialog.component.ts index b1ca1922775..3afd76e86ce 100644 --- a/apps/web/src/app/billing/payment/components/require-payment-method-dialog.component.ts +++ b/apps/web/src/app/billing/payment/components/require-payment-method-dialog.component.ts @@ -29,6 +29,8 @@ type DialogParams = { }; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: ` diff --git a/apps/web/src/app/billing/payment/components/submit-payment-method-dialog.component.ts b/apps/web/src/app/billing/payment/components/submit-payment-method-dialog.component.ts index cc1f1ab5e0a..98e8ba99e5e 100644 --- a/apps/web/src/app/billing/payment/components/submit-payment-method-dialog.component.ts +++ b/apps/web/src/app/billing/payment/components/submit-payment-method-dialog.component.ts @@ -14,8 +14,12 @@ export type SubmitPaymentMethodDialogResult = | { type: "error" } | { type: "success"; paymentMethod: MaskedPaymentMethod }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: "" }) export abstract class SubmitPaymentMethodDialogComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild(EnterPaymentMethodComponent) private enterPaymentMethodComponent!: EnterPaymentMethodComponent; protected formGroup = EnterPaymentMethodComponent.getFormGroup(); diff --git a/apps/web/src/app/billing/payment/components/verify-bank-account.component.ts b/apps/web/src/app/billing/payment/components/verify-bank-account.component.ts index b1a2814daf2..5e61cf5b129 100644 --- a/apps/web/src/app/billing/payment/components/verify-bank-account.component.ts +++ b/apps/web/src/app/billing/payment/components/verify-bank-account.component.ts @@ -9,6 +9,8 @@ import { SharedModule } from "../../../shared"; import { BitwardenSubscriber } from "../../types"; import { MaskedPaymentMethod } from "../types"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-verify-bank-account", template: ` @@ -35,7 +37,11 @@ import { MaskedPaymentMethod } from "../types"; providers: [SubscriberBillingClient], }) export class VerifyBankAccountComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) subscriber!: BitwardenSubscriber; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() verified = new EventEmitter(); protected formGroup = new FormGroup({ diff --git a/apps/web/src/app/billing/settings/sponsored-families.component.ts b/apps/web/src/app/billing/settings/sponsored-families.component.ts index 80e66784ae8..530db0ff397 100644 --- a/apps/web/src/app/billing/settings/sponsored-families.component.ts +++ b/apps/web/src/app/billing/settings/sponsored-families.component.ts @@ -33,6 +33,8 @@ interface RequestSponsorshipForm { sponsorshipEmail: FormControl; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-sponsored-families", templateUrl: "sponsored-families.component.html", diff --git a/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts b/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts index 70320e7e62e..6d27130025d 100644 --- a/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts +++ b/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts @@ -15,15 +15,23 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { DialogService, ToastService } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "[sponsoring-org-row]", templateUrl: "sponsoring-org-row.component.html", standalone: false, }) export class SponsoringOrgRowComponent implements OnInit { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() sponsoringOrg: Organization = null; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() isSelfHosted = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() sponsorshipRemoved = new EventEmitter(); statusMessage = "loading"; diff --git a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts index 1f9172eaf59..a9857588e1c 100644 --- a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts +++ b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts @@ -29,6 +29,8 @@ export enum AdjustStorageDialogResultType { Closed = "closed", } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "./adjust-storage-dialog.component.html", standalone: false, diff --git a/apps/web/src/app/billing/shared/billing-free-families-nav-item.component.ts b/apps/web/src/app/billing/shared/billing-free-families-nav-item.component.ts index 60b46c2b64e..00d4a7835e5 100644 --- a/apps/web/src/app/billing/shared/billing-free-families-nav-item.component.ts +++ b/apps/web/src/app/billing/shared/billing-free-families-nav-item.component.ts @@ -7,6 +7,8 @@ import { FreeFamiliesPolicyService } from "../services/free-families-policy.serv import { BillingSharedModule } from "./billing-shared.module"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "billing-free-families-nav-item", templateUrl: "./billing-free-families-nav-item.component.html", diff --git a/apps/web/src/app/billing/shared/billing-history.component.ts b/apps/web/src/app/billing/shared/billing-history.component.ts index 745939f0d5e..a5d8d7e3da7 100644 --- a/apps/web/src/app/billing/shared/billing-history.component.ts +++ b/apps/web/src/app/billing/shared/billing-history.component.ts @@ -8,18 +8,26 @@ import { BillingTransactionResponse, } from "@bitwarden/common/billing/models/response/billing.response"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-billing-history", templateUrl: "billing-history.component.html", standalone: false, }) export class BillingHistoryComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() openInvoices: BillingInvoiceResponse[]; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() paidInvoices: BillingInvoiceResponse[]; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() transactions: BillingTransactionResponse[]; diff --git a/apps/web/src/app/billing/shared/offboarding-survey.component.ts b/apps/web/src/app/billing/shared/offboarding-survey.component.ts index 9f21f2b8cd5..fe7d724a079 100644 --- a/apps/web/src/app/billing/shared/offboarding-survey.component.ts +++ b/apps/web/src/app/billing/shared/offboarding-survey.component.ts @@ -46,6 +46,8 @@ export const openOffboardingSurvey = ( dialogConfig, ); +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-cancel-subscription-form", templateUrl: "offboarding-survey.component.html", diff --git a/apps/web/src/app/billing/shared/plan-card/plan-card.component.ts b/apps/web/src/app/billing/shared/plan-card/plan-card.component.ts index 4150ddc25ba..0c64d078757 100644 --- a/apps/web/src/app/billing/shared/plan-card/plan-card.component.ts +++ b/apps/web/src/app/billing/shared/plan-card/plan-card.component.ts @@ -11,13 +11,15 @@ export interface PlanCard { productTier: ProductTierType; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-plan-card", templateUrl: "./plan-card.component.html", standalone: false, }) export class PlanCardComponent { - plan = input.required(); + readonly plan = input.required(); productTiers = ProductTierType; cardClicked = output(); diff --git a/apps/web/src/app/billing/shared/pricing-summary/pricing-summary.component.ts b/apps/web/src/app/billing/shared/pricing-summary/pricing-summary.component.ts index d4fdf35b743..f502297425a 100644 --- a/apps/web/src/app/billing/shared/pricing-summary/pricing-summary.component.ts +++ b/apps/web/src/app/billing/shared/pricing-summary/pricing-summary.component.ts @@ -31,12 +31,16 @@ export interface PricingSummaryData { estimatedTax?: number; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-pricing-summary", templateUrl: "./pricing-summary.component.html", standalone: false, }) export class PricingSummaryComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() summaryData!: PricingSummaryData; planIntervals = PlanInterval; diff --git a/apps/web/src/app/billing/shared/self-hosting-license-uploader/individual-self-hosting-license-uploader.component.ts b/apps/web/src/app/billing/shared/self-hosting-license-uploader/individual-self-hosting-license-uploader.component.ts index 75da10a7b09..8c4010d2117 100644 --- a/apps/web/src/app/billing/shared/self-hosting-license-uploader/individual-self-hosting-license-uploader.component.ts +++ b/apps/web/src/app/billing/shared/self-hosting-license-uploader/individual-self-hosting-license-uploader.component.ts @@ -14,6 +14,8 @@ import { AbstractSelfHostingLicenseUploaderComponent } from "../../shared/self-h * Processes license file uploads for individual plans. * @remarks Requires self-hosting. */ +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "individual-self-hosting-license-uploader", templateUrl: "./self-hosting-license-uploader.component.html", @@ -23,6 +25,8 @@ export class IndividualSelfHostingLicenseUploaderComponent extends AbstractSelfH /** * Emitted when a license file has been successfully uploaded & processed. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onLicenseFileUploaded: EventEmitter = new EventEmitter(); constructor( diff --git a/apps/web/src/app/billing/shared/self-hosting-license-uploader/organization-self-hosting-license-uploader.component.ts b/apps/web/src/app/billing/shared/self-hosting-license-uploader/organization-self-hosting-license-uploader.component.ts index e2b43a6a568..892a42ef61c 100644 --- a/apps/web/src/app/billing/shared/self-hosting-license-uploader/organization-self-hosting-license-uploader.component.ts +++ b/apps/web/src/app/billing/shared/self-hosting-license-uploader/organization-self-hosting-license-uploader.component.ts @@ -24,6 +24,8 @@ import { AbstractSelfHostingLicenseUploaderComponent } from "../../shared/self-h * Processes license file uploads for organizations. * @remarks Requires self-hosting. */ +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "organization-self-hosting-license-uploader", templateUrl: "./self-hosting-license-uploader.component.html", @@ -33,6 +35,8 @@ export class OrganizationSelfHostingLicenseUploaderComponent extends AbstractSel /** * Notifies the parent component of the `organizationId` the license was created for. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onLicenseFileUploaded: EventEmitter = new EventEmitter(); constructor( diff --git a/apps/web/src/app/billing/shared/sm-subscribe.component.ts b/apps/web/src/app/billing/shared/sm-subscribe.component.ts index d1e5566a235..739cc6f1451 100644 --- a/apps/web/src/app/billing/shared/sm-subscribe.component.ts +++ b/apps/web/src/app/billing/shared/sm-subscribe.component.ts @@ -29,16 +29,28 @@ export const secretsManagerSubscribeFormFactory = ( ], }); +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "sm-subscribe", templateUrl: "sm-subscribe.component.html", standalone: false, }) export class SecretsManagerSubscribeComponent implements OnInit, OnDestroy { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() formGroup: FormGroup>; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() upgradeOrganization: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() showSubmitButton = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() selectedPlan: PlanResponse; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() customerDiscount: BillingCustomerDiscount; logo = SecretsManagerAlt; diff --git a/apps/web/src/app/billing/shared/trial-payment-dialog/trial-payment-dialog.component.ts b/apps/web/src/app/billing/shared/trial-payment-dialog/trial-payment-dialog.component.ts index ed59e2a2d97..64af7be948e 100644 --- a/apps/web/src/app/billing/shared/trial-payment-dialog/trial-payment-dialog.component.ts +++ b/apps/web/src/app/billing/shared/trial-payment-dialog/trial-payment-dialog.component.ts @@ -67,6 +67,8 @@ interface OnSuccessArgs { organizationId: string; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-trial-payment-dialog", templateUrl: "./trial-payment-dialog.component.html", @@ -74,6 +76,8 @@ interface OnSuccessArgs { providers: [SubscriberBillingClient, TaxClient], }) export class TrialPaymentDialogComponent implements OnInit, OnDestroy { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild(EnterPaymentMethodComponent) enterPaymentMethodComponent!: EnterPaymentMethodComponent; currentPlan!: PlanResponse; @@ -84,9 +88,11 @@ export class TrialPaymentDialogComponent implements OnInit, OnDestroy { sub!: OrganizationSubscriptionResponse; selectedInterval: PlanInterval = PlanInterval.Annually; - planCards = signal([]); + readonly planCards = signal([]); plans!: ListResponse; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onSuccess = new EventEmitter(); protected initialPaymentMethod: PaymentMethodType; protected readonly ResultType = TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE; diff --git a/apps/web/src/app/billing/shared/update-license-dialog.component.ts b/apps/web/src/app/billing/shared/update-license-dialog.component.ts index 11b5e7fd8df..d9c885c9819 100644 --- a/apps/web/src/app/billing/shared/update-license-dialog.component.ts +++ b/apps/web/src/app/billing/shared/update-license-dialog.component.ts @@ -10,6 +10,8 @@ import { DialogRef, DialogService, ToastService } from "@bitwarden/components"; import { UpdateLicenseDialogResult } from "./update-license-types"; import { UpdateLicenseComponent } from "./update-license.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "update-license-dialog.component.html", standalone: false, diff --git a/apps/web/src/app/billing/shared/update-license.component.ts b/apps/web/src/app/billing/shared/update-license.component.ts index 455b38386c6..fa42c116184 100644 --- a/apps/web/src/app/billing/shared/update-license.component.ts +++ b/apps/web/src/app/billing/shared/update-license.component.ts @@ -12,16 +12,28 @@ import { ToastService } from "@bitwarden/components"; import { UpdateLicenseDialogResult } from "./update-license-types"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-update-license", templateUrl: "update-license.component.html", standalone: false, }) export class UpdateLicenseComponent implements OnInit { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() organizationId: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() showCancel = true; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() showAutomaticSyncAndManualUpload: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onUpdated = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onCanceled = new EventEmitter(); formPromise: Promise; diff --git a/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts b/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts index baccabdc763..19fa023a5b2 100644 --- a/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts +++ b/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts @@ -40,12 +40,16 @@ export type InitiationPath = | "Password Manager trial from marketing website" | "Secrets Manager trial from marketing website"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-complete-trial-initiation", templateUrl: "complete-trial-initiation.component.html", standalone: false, }) export class CompleteTrialInitiationComponent implements OnInit, OnDestroy { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("stepper", { static: false }) verticalStepper!: VerticalStepperComponent; inputPasswordFlow = InputPasswordFlow.SetInitialPasswordAccountRegistration; diff --git a/apps/web/src/app/billing/trial-initiation/confirmation-details.component.ts b/apps/web/src/app/billing/trial-initiation/confirmation-details.component.ts index cbb1c84284c..3c92749dd38 100644 --- a/apps/web/src/app/billing/trial-initiation/confirmation-details.component.ts +++ b/apps/web/src/app/billing/trial-initiation/confirmation-details.component.ts @@ -4,15 +4,25 @@ import { Component, Input } from "@angular/core"; import { ProductType } from "@bitwarden/common/billing/enums"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-trial-confirmation-details", templateUrl: "confirmation-details.component.html", standalone: false, }) export class ConfirmationDetailsComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() email: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() orgLabel: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() product?: ProductType = ProductType.PasswordManager; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() trialLength: number; protected readonly Product = ProductType; diff --git a/apps/web/src/app/billing/trial-initiation/trial-billing-step/trial-billing-step.component.ts b/apps/web/src/app/billing/trial-initiation/trial-billing-step/trial-billing-step.component.ts index 0f185564c2e..04ee7931cf3 100644 --- a/apps/web/src/app/billing/trial-initiation/trial-billing-step/trial-billing-step.component.ts +++ b/apps/web/src/app/billing/trial-initiation/trial-billing-step/trial-billing-step.component.ts @@ -35,6 +35,8 @@ export interface OrganizationCreatedEvent { planDescription: string; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-trial-billing-step", templateUrl: "./trial-billing-step.component.html", @@ -42,8 +44,12 @@ export interface OrganizationCreatedEvent { providers: [TaxClient, TrialBillingStepService], }) export class TrialBillingStepComponent implements OnInit, OnDestroy { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild(EnterPaymentMethodComponent) enterPaymentMethodComponent!: EnterPaymentMethodComponent; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals protected trial = input.required(); protected steppedBack = output(); protected organizationCreated = output(); diff --git a/apps/web/src/app/billing/trial-initiation/vertical-stepper/vertical-step-content.component.ts b/apps/web/src/app/billing/trial-initiation/vertical-stepper/vertical-step-content.component.ts index 0c6e084f5c4..183346b9033 100644 --- a/apps/web/src/app/billing/trial-initiation/vertical-stepper/vertical-step-content.component.ts +++ b/apps/web/src/app/billing/trial-initiation/vertical-stepper/vertical-step-content.component.ts @@ -4,17 +4,29 @@ import { Component, EventEmitter, Input, Output } from "@angular/core"; import { VerticalStep } from "./vertical-step.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-vertical-step-content", templateUrl: "vertical-step-content.component.html", standalone: false, }) export class VerticalStepContentComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onSelectStep = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() disabled = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() selected = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() step: VerticalStep; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() stepNumber: number; selectStep() { diff --git a/apps/web/src/app/billing/trial-initiation/vertical-stepper/vertical-step.component.ts b/apps/web/src/app/billing/trial-initiation/vertical-stepper/vertical-step.component.ts index b4b643b3889..efd0f68e5d1 100644 --- a/apps/web/src/app/billing/trial-initiation/vertical-stepper/vertical-step.component.ts +++ b/apps/web/src/app/billing/trial-initiation/vertical-stepper/vertical-step.component.ts @@ -1,6 +1,8 @@ import { CdkStep } from "@angular/cdk/stepper"; import { Component, Input } from "@angular/core"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-vertical-step", templateUrl: "vertical-step.component.html", @@ -8,7 +10,13 @@ import { Component, Input } from "@angular/core"; standalone: false, }) export class VerticalStep extends CdkStep { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() subLabel = ""; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() applyBorder = true; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() addSubLabelSpacing = false; } diff --git a/apps/web/src/app/billing/trial-initiation/vertical-stepper/vertical-stepper.component.ts b/apps/web/src/app/billing/trial-initiation/vertical-stepper/vertical-stepper.component.ts index 333224aac54..c7c2c17000e 100644 --- a/apps/web/src/app/billing/trial-initiation/vertical-stepper/vertical-stepper.component.ts +++ b/apps/web/src/app/billing/trial-initiation/vertical-stepper/vertical-stepper.component.ts @@ -5,6 +5,8 @@ import { Component, Input, QueryList } from "@angular/core"; import { VerticalStep } from "./vertical-step.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-vertical-stepper", templateUrl: "vertical-stepper.component.html", @@ -14,6 +16,8 @@ import { VerticalStep } from "./vertical-step.component"; export class VerticalStepperComponent extends CdkStepper { readonly steps: QueryList; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() activeClass = "active"; diff --git a/apps/web/src/app/billing/warnings/components/tax-id-warning.component.ts b/apps/web/src/app/billing/warnings/components/tax-id-warning.component.ts index 55fa0c0f439..c0fe5626fcb 100644 --- a/apps/web/src/app/billing/warnings/components/tax-id-warning.component.ts +++ b/apps/web/src/app/billing/warnings/components/tax-id-warning.component.ts @@ -83,6 +83,8 @@ type View = { type GetWarning$ = () => Observable; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-tax-id-warning", template: ` @@ -108,8 +110,14 @@ type GetWarning$ = () => Observable; imports: [BannerModule, SharedModule], }) export class TaxIdWarningComponent implements OnInit { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) subscriber!: NonIndividualSubscriber; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) getWarning$!: GetWarning$; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() billingAddressUpdated = new EventEmitter(); protected enableTaxIdWarning$ = this.configService.getFeatureFlag$( diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/add-existing-organization-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/add-existing-organization-dialog.component.ts index a99d86b6e96..e36e4e5f0c6 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/add-existing-organization-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/add-existing-organization-dialog.component.ts @@ -25,6 +25,8 @@ export enum AddExistingOrganizationDialogResultType { Submitted = "submitted", } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "./add-existing-organization-dialog.component.html", standalone: false, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/create-client-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/create-client-dialog.component.ts index 73e642dfa06..917ccf58e46 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/create-client-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/create-client-dialog.component.ts @@ -100,6 +100,8 @@ export class PlanCard { } } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "./create-client-dialog.component.html", standalone: false, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/manage-client-name-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/manage-client-name-dialog.component.ts index 045c9d8e8df..7e093fdad9b 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/manage-client-name-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/manage-client-name-dialog.component.ts @@ -39,6 +39,8 @@ export const openManageClientNameDialog = ( dialogConfig, ); +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "manage-client-name-dialog.component.html", standalone: false, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/manage-client-subscription-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/manage-client-subscription-dialog.component.ts index 4c80402d3f7..9e74a91a4c0 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/manage-client-subscription-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/manage-client-subscription-dialog.component.ts @@ -35,6 +35,8 @@ export const openManageClientSubscriptionDialog = ( ManageClientSubscriptionDialogParams >(ManageClientSubscriptionDialogComponent, dialogConfig); +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "./manage-client-subscription-dialog.component.html", standalone: false, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/manage-clients.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/manage-clients.component.ts index a3601d2c812..eed3db87396 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/manage-clients.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/manage-clients.component.ts @@ -57,6 +57,8 @@ import { import { NoClientsComponent } from "./no-clients.component"; import { ReplacePipe } from "./replace.pipe"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "manage-clients.component.html", imports: [ diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/no-clients.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/no-clients.component.ts index ed11eb8ef0a..f78e8ae38f2 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/no-clients.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/no-clients.component.ts @@ -4,6 +4,8 @@ import { GearIcon } from "@bitwarden/assets/svg"; import { NoItemsModule } from "@bitwarden/components"; import { SharedOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/shared"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-no-clients", imports: [SharedOrganizationModule, NoItemsModule], @@ -27,8 +29,14 @@ import { SharedOrganizationModule } from "@bitwarden/web-vault/app/admin-console }) export class NoClientsComponent { icon = GearIcon; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() showAddOrganizationButton = true; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() disableAddOrganizationButton = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() addNewOrganizationClicked = new EventEmitter(); addNewOrganization = () => this.addNewOrganizationClicked.emit(); diff --git a/bitwarden_license/bit-web/src/app/billing/policies/free-families-sponsorship.component.ts b/bitwarden_license/bit-web/src/app/billing/policies/free-families-sponsorship.component.ts index db5ef3ba62f..d1c6c820547 100644 --- a/bitwarden_license/bit-web/src/app/billing/policies/free-families-sponsorship.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/policies/free-families-sponsorship.component.ts @@ -14,6 +14,8 @@ export class FreeFamiliesSponsorshipPolicy extends BasePolicyEditDefinition { component = FreeFamiliesSponsorshipPolicyComponent; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "free-families-sponsorship.component.html", imports: [SharedModule], diff --git a/bitwarden_license/bit-web/src/app/billing/providers/billing-history/invoices.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/billing-history/invoices.component.ts index fc3352048d6..6c607d205b6 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/billing-history/invoices.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/billing-history/invoices.component.ts @@ -8,15 +8,25 @@ import { } from "@bitwarden/common/billing/models/response/invoices.response"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-invoices", templateUrl: "./invoices.component.html", standalone: false, }) export class InvoicesComponent implements OnInit { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() startWith?: InvoicesResponse; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() getInvoices?: () => Promise; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() getClientInvoiceReport?: (invoiceId: string) => Promise; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() getClientInvoiceReportName?: (invoiceResponse: InvoiceResponse) => string; protected invoices: InvoiceResponse[] = []; diff --git a/bitwarden_license/bit-web/src/app/billing/providers/billing-history/no-invoices.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/billing-history/no-invoices.component.ts index ded6bc79593..882a2c764ac 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/billing-history/no-invoices.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/billing-history/no-invoices.component.ts @@ -2,6 +2,8 @@ import { Component } from "@angular/core"; import { CreditCardIcon } from "@bitwarden/assets/svg"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-no-invoices", template: ` diff --git a/bitwarden_license/bit-web/src/app/billing/providers/billing-history/provider-billing-history.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/billing-history/provider-billing-history.component.ts index d1a9d43a6fc..5823080bd3b 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/billing-history/provider-billing-history.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/billing-history/provider-billing-history.component.ts @@ -10,6 +10,8 @@ import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstract import { InvoiceResponse } from "@bitwarden/common/billing/models/response/invoices.response"; import { BillingNotificationService } from "@bitwarden/web-vault/app/billing/services/billing-notification.service"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "./provider-billing-history.component.html", standalone: false, diff --git a/bitwarden_license/bit-web/src/app/billing/providers/payment-details/provider-payment-details.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/payment-details/provider-payment-details.component.ts index 5a070687de4..183e6098471 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/payment-details/provider-payment-details.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/payment-details/provider-payment-details.component.ts @@ -59,6 +59,8 @@ const BANK_ACCOUNT_VERIFIED_COMMAND = new CommandDefinition<{ adminId: string; }>("providerBankAccountVerified"); +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "./provider-payment-details.component.html", imports: [ diff --git a/bitwarden_license/bit-web/src/app/billing/providers/setup/setup-business-unit.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/setup/setup-business-unit.component.ts index a3f8acd6488..4b8dfce05d5 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/setup/setup-business-unit.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/setup/setup-business-unit.component.ts @@ -17,6 +17,8 @@ import { KeyService } from "@bitwarden/key-management"; import { BillingNotificationService } from "@bitwarden/web-vault/app/billing/services/billing-notification.service"; import { BaseAcceptComponent } from "@bitwarden/web-vault/app/common/base.accept.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "./setup-business-unit.component.html", standalone: false, diff --git a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription-status.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription-status.component.ts index f9ff006de24..dfbfdb29eef 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription-status.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription-status.component.ts @@ -23,12 +23,16 @@ type ComponentData = { }; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-provider-subscription-status", templateUrl: "provider-subscription-status.component.html", standalone: false, }) export class ProviderSubscriptionStatusComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) subscription: ProviderSubscriptionResponse; constructor( diff --git a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts index 98aceb0f878..2e43ce966d3 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts @@ -11,6 +11,8 @@ import { } from "@bitwarden/common/billing/models/response/provider-subscription-response"; import { BillingNotificationService } from "@bitwarden/web-vault/app/billing/services/billing-notification.service"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-provider-subscription", templateUrl: "./provider-subscription.component.html", diff --git a/libs/angular/src/billing/components/premium-badge/premium-badge.component.ts b/libs/angular/src/billing/components/premium-badge/premium-badge.component.ts index a4a1d76d1d6..e8a829d458d 100644 --- a/libs/angular/src/billing/components/premium-badge/premium-badge.component.ts +++ b/libs/angular/src/billing/components/premium-badge/premium-badge.component.ts @@ -4,6 +4,8 @@ import { JslibModule } from "@bitwarden/angular/jslib.module"; import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; import { BadgeModule } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-premium-badge", standalone: true, @@ -15,7 +17,7 @@ import { BadgeModule } from "@bitwarden/components"; imports: [BadgeModule, JslibModule], }) export class PremiumBadgeComponent { - organizationId = input(); + readonly organizationId = input(); constructor(private premiumUpgradePromptService: PremiumUpgradePromptService) {} diff --git a/libs/pricing/src/components/cart-summary/cart-summary.component.ts b/libs/pricing/src/components/cart-summary/cart-summary.component.ts index b21276b5038..11c6cddcab1 100644 --- a/libs/pricing/src/components/cart-summary/cart-summary.component.ts +++ b/libs/pricing/src/components/cart-summary/cart-summary.component.ts @@ -16,6 +16,8 @@ export type LineItem = { * This component has no external dependencies and performs minimal logic - * it only displays data and allows expanding/collapsing of line items. */ +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "billing-cart-summary", templateUrl: "./cart-summary.component.html", @@ -23,13 +25,13 @@ export type LineItem = { }) export class CartSummaryComponent { // Required inputs - passwordManager = input.required(); - additionalStorage = input(); - secretsManager = input<{ seats: LineItem; additionalServiceAccounts?: LineItem }>(); - estimatedTax = input.required(); + readonly passwordManager = input.required(); + readonly additionalStorage = input(); + readonly secretsManager = input<{ seats: LineItem; additionalServiceAccounts?: LineItem }>(); + readonly estimatedTax = input.required(); // UI state - isExpanded = signal(true); + readonly isExpanded = signal(true); /** * Calculates total for password manager line item diff --git a/libs/pricing/src/components/pricing-card/pricing-card.component.spec.ts b/libs/pricing/src/components/pricing-card/pricing-card.component.spec.ts index ed2c28d8cb3..df60d7647f7 100644 --- a/libs/pricing/src/components/pricing-card/pricing-card.component.spec.ts +++ b/libs/pricing/src/components/pricing-card/pricing-card.component.spec.ts @@ -6,6 +6,8 @@ import { ButtonType, IconModule, TypographyModule } from "@bitwarden/components" import { PricingCardComponent } from "./pricing-card.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: ` (); readonly activeBadge = input<{ text: string; variant?: BadgeVariant }>(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() buttonClick = new EventEmitter(); /** From 2eef32d757615d48283360aabd578448575c1f10 Mon Sep 17 00:00:00 2001 From: Stephon Brown Date: Thu, 23 Oct 2025 17:21:48 -0400 Subject: [PATCH 21/73] fix(billing): add condition to disable submit button for account credit (#17006) --- .../upgrade/upgrade-payment/upgrade-payment.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.html b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.html index 2228a6f6c06..9b007ae7a6b 100644 --- a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.html +++ b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.html @@ -70,7 +70,7 @@ bitButton bitFormButton buttonType="primary" - [disabled]="loading() || !isFormValid()" + [disabled]="loading() || !isFormValid() || !(hasEnoughAccountCredit$ | async)" type="submit" > {{ "upgrade" | i18n }} From ce84d2f117eb6d57b55733c3d3279256f673d8d4 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Thu, 23 Oct 2025 15:02:37 -0700 Subject: [PATCH 22/73] fix(sso-config): (Auth) [PM-27244] Refactor KC URL Handling (#16995) Addresses some bugs with the Key Connector URL form field. --- .../src/app/auth/sso/sso.component.html | 4 +- .../bit-web/src/app/auth/sso/sso.component.ts | 216 +++++++++++------- 2 files changed, 133 insertions(+), 87 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html index 6d2836ee0ba..db2e000246b 100644 --- a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html +++ b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html @@ -1,7 +1,7 @@ - + {{ "loading" | i18n }} - +

{{ "ssoPolicyHelpStart" | i18n }} {{ "ssoPolicyHelpAnchor" | i18n }} diff --git a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts index 4928d7a6abc..1c25283ea4f 100644 --- a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts +++ b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts @@ -9,15 +9,7 @@ import { Validators, } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; -import { - concatMap, - firstValueFrom, - pairwise, - startWith, - Subject, - switchMap, - takeUntil, -} from "rxjs"; +import { concatMap, firstValueFrom, Subject, Subscription, switchMap, takeUntil } from "rxjs"; import { ControlsOf } from "@bitwarden/angular/types/controls-of"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -45,8 +37,10 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service"; 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 { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ToastService } from "@bitwarden/components"; +import { LogService } from "@bitwarden/logging"; import { ssoTypeValidator } from "./sso-type.validator"; @@ -120,7 +114,11 @@ export class SsoComponent implements OnInit, OnDestroy { showOpenIdCustomizations = false; - loading = true; + isInitializing = true; // concerned with UI/UX (i.e. when to show loading spinner vs form) + isFormValidatingOrPopulating = true; // tracks when form fields are being validated/populated during load() or submit() + + configuredKeyConnectorUrlFromServer: string | null; + memberDecryptionTypeValueChangesSubscription: Subscription | null = null; haveTestedKeyConnector = false; organizationId: string; organization: Organization; @@ -215,6 +213,8 @@ export class SsoComponent implements OnInit, OnDestroy { private organizationApiService: OrganizationApiServiceAbstraction, private toastService: ToastService, private environmentService: EnvironmentService, + private validationService: ValidationService, + private logService: LogService, ) {} async ngOnInit() { @@ -265,41 +265,6 @@ export class SsoComponent implements OnInit, OnDestroy { .subscribe(); this.showKeyConnectorOptions = this.platformUtilsService.isSelfHost(); - - // Only setup listener if key connector is a possible selection - if (this.showKeyConnectorOptions) { - this.listenForKeyConnectorSelection(); - } - } - - listenForKeyConnectorSelection() { - const memberDecryptionTypeOnInit = this.ssoConfigForm?.controls?.memberDecryptionType.value; - - this.ssoConfigForm?.controls?.memberDecryptionType.valueChanges - .pipe( - startWith(memberDecryptionTypeOnInit), - pairwise(), - switchMap(async ([prevMemberDecryptionType, newMemberDecryptionType]) => { - // Only pre-populate a default URL when changing TO Key Connector from a different decryption type. - // ValueChanges gets re-triggered during the submit() call, so we need a !== check - // to prevent a custom URL from getting overwritten back to the default on a submit(). - if ( - prevMemberDecryptionType !== MemberDecryptionType.KeyConnector && - newMemberDecryptionType === MemberDecryptionType.KeyConnector - ) { - // Pre-populate a default key connector URL (user can still change it) - const env = await firstValueFrom(this.environmentService.environment$); - const webVaultUrl = env.getWebVaultUrl(); - const defaultKeyConnectorUrl = webVaultUrl + "/key-connector"; - - this.ssoConfigForm.controls.keyConnectorUrl.setValue(defaultKeyConnectorUrl); - } else if (newMemberDecryptionType !== MemberDecryptionType.KeyConnector) { - this.ssoConfigForm.controls.keyConnectorUrl.setValue(""); - } - }), - takeUntil(this.destroy$), - ) - .subscribe(); } ngOnDestroy(): void { @@ -308,55 +273,135 @@ export class SsoComponent implements OnInit, OnDestroy { } async load() { - const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); - this.organization = await firstValueFrom( - this.organizationService - .organizations$(userId) - .pipe(getOrganizationById(this.organizationId)), - ); - const ssoSettings = await this.organizationApiService.getSso(this.organizationId); - this.populateForm(ssoSettings); + // Even though these component properties were initialized to true, we must always reset + // them to true at the top of this method in case an admin navigates to another org via + // the browser address bar, which re-executes load() on the same component instance + // (not a new instance). + this.isInitializing = true; + this.isFormValidatingOrPopulating = true; + // Same with unsubscribing: re-executing load() on the same component instance (not a new + // instance) means we will not unsubscribe via takeUntil(this.destroy$). We must manually + // unsubscribe for this case. We unsubscribe here in case the try block fails. + this.memberDecryptionTypeValueChangesSubscription?.unsubscribe(); + this.memberDecryptionTypeValueChangesSubscription = null; - this.callbackPath = ssoSettings.urls.callbackPath; - this.signedOutCallbackPath = ssoSettings.urls.signedOutCallbackPath; - this.spEntityId = ssoSettings.urls.spEntityId; - this.spEntityIdStatic = ssoSettings.urls.spEntityIdStatic; - this.spMetadataUrl = ssoSettings.urls.spMetadataUrl; - this.spAcsUrl = ssoSettings.urls.spAcsUrl; + try { + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + this.organization = await firstValueFrom( + this.organizationService + .organizations$(userId) + .pipe(getOrganizationById(this.organizationId)), + ); + const ssoSettings = await this.organizationApiService.getSso(this.organizationId); + this.configuredKeyConnectorUrlFromServer = ssoSettings.data?.keyConnectorUrl; + this.populateForm(ssoSettings); - this.loading = false; + this.callbackPath = ssoSettings.urls.callbackPath; + this.signedOutCallbackPath = ssoSettings.urls.signedOutCallbackPath; + this.spEntityId = ssoSettings.urls.spEntityId; + this.spEntityIdStatic = ssoSettings.urls.spEntityIdStatic; + this.spMetadataUrl = ssoSettings.urls.spMetadataUrl; + this.spAcsUrl = ssoSettings.urls.spAcsUrl; + + if (this.showKeyConnectorOptions) { + // We don't setup this subscription until AFTER the form has been populated on load(). + // This is because populateForm() will trigger valueChanges, but we don't want to + // listen for or react to valueChanges until AFTER the form has had a chance to be + // populated with already configured values retrieved from the server. + this.subscribeToMemberDecryptionTypeValueChanges(); + } + } catch (error) { + this.logService.error("Error loading SSO configuration: ", error); + this.validationService.showError(error); + } finally { + this.isInitializing = false; + this.isFormValidatingOrPopulating = false; + } } submit = async () => { - this.updateFormValidationState(this.ssoConfigForm); + this.isFormValidatingOrPopulating = true; - if (this.ssoConfigForm.value.memberDecryptionType === MemberDecryptionType.KeyConnector) { - this.haveTestedKeyConnector = false; - await this.validateKeyConnectorUrl(); + try { + this.updateFormValidationState(this.ssoConfigForm); + + if (this.ssoConfigForm.value.memberDecryptionType === MemberDecryptionType.KeyConnector) { + this.haveTestedKeyConnector = false; + await this.validateKeyConnectorUrl(); + } + + if (!this.ssoConfigForm.valid) { + this.readOutErrors(); + return; + } + const request = new OrganizationSsoRequest(); + request.enabled = this.enabledCtrl.value; + // Return null instead of empty string to avoid duplicate id errors in database + request.identifier = + this.ssoIdentifierCtrl.value === "" ? null : this.ssoIdentifierCtrl.value; + request.data = SsoConfigApi.fromView(this.ssoConfigForm.getRawValue()); + + const response = await this.organizationApiService.updateSso(this.organizationId, request); + this.configuredKeyConnectorUrlFromServer = response.data?.keyConnectorUrl; + this.populateForm(response); + + await this.upsertOrganizationWithSsoChanges(request); + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("ssoSettingsSaved"), + }); + } finally { + this.isFormValidatingOrPopulating = false; } - - if (!this.ssoConfigForm.valid) { - this.readOutErrors(); - return; - } - const request = new OrganizationSsoRequest(); - request.enabled = this.enabledCtrl.value; - // Return null instead of empty string to avoid duplicate id errors in database - request.identifier = this.ssoIdentifierCtrl.value === "" ? null : this.ssoIdentifierCtrl.value; - request.data = SsoConfigApi.fromView(this.ssoConfigForm.getRawValue()); - - const response = await this.organizationApiService.updateSso(this.organizationId, request); - this.populateForm(response); - - await this.upsertOrganizationWithSsoChanges(request); - - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("ssoSettingsSaved"), - }); }; + private subscribeToMemberDecryptionTypeValueChanges() { + // The load() method will have unsubscribed from any pre-existing subscription before + // we setup a new subscription here. + + this.memberDecryptionTypeValueChangesSubscription = + this.ssoConfigForm?.controls?.memberDecryptionType.valueChanges + .pipe( + switchMap(async (memberDecryptionType: MemberDecryptionType) => { + this.haveTestedKeyConnector = false; + + if (this.isFormValidatingOrPopulating) { + // If the form is being validated/populated due to a load() or submit() call (both of which + // trigger valueChanges) we don't want to react to this valueChanges emission. + return; + } + + if (memberDecryptionType === MemberDecryptionType.KeyConnector) { + if (this.configuredKeyConnectorUrlFromServer) { + // If the user already has a key connector URL configured, it will have been retrieved + // from the server and set to the form field upon load(). But if this user then selects a + // different Member Decryption option (but does not save the form), and then once again + // selects the Key Connector option, we want to pre-populate the form field with the already + // configured URL that was originally retreived from the server, not a default URL. + this.ssoConfigForm.controls.keyConnectorUrl.setValue( + this.configuredKeyConnectorUrlFromServer, + ); + return; + } + + // Pre-populate a default key connector URL (user can still change it) + const env = await firstValueFrom(this.environmentService.environment$); + const webVaultUrl = env.getWebVaultUrl(); + const defaultKeyConnectorUrl = webVaultUrl + "/key-connector"; + + this.ssoConfigForm.controls.keyConnectorUrl.setValue(defaultKeyConnectorUrl); + } else { + // Clear the key connector url + this.ssoConfigForm.controls.keyConnectorUrl.setValue(""); + } + }), + takeUntil(this.destroy$), + ) + .subscribe(); + } + async validateKeyConnectorUrl() { if (this.haveTestedKeyConnector) { return; @@ -371,6 +416,7 @@ export class SsoComponent implements OnInit, OnDestroy { this.keyConnectorUrl.setErrors({ invalidUrl: { message: this.i18nService.t("keyConnectorTestFail") }, }); + this.keyConnectorUrl.markAllAsTouched(); } this.haveTestedKeyConnector = true; From 1b685e3b7e367f271f4e985056e80dec210d6e7d Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Fri, 24 Oct 2025 08:45:13 +0200 Subject: [PATCH 23/73] Autosync the updated translations (#17010) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/ar/messages.json | 3 + apps/browser/src/_locales/az/messages.json | 3 + apps/browser/src/_locales/be/messages.json | 3 + apps/browser/src/_locales/bg/messages.json | 3 + apps/browser/src/_locales/bn/messages.json | 5 +- apps/browser/src/_locales/bs/messages.json | 3 + apps/browser/src/_locales/ca/messages.json | 55 ++++++++++--------- apps/browser/src/_locales/cs/messages.json | 3 + apps/browser/src/_locales/cy/messages.json | 3 + apps/browser/src/_locales/da/messages.json | 3 + apps/browser/src/_locales/de/messages.json | 3 + apps/browser/src/_locales/el/messages.json | 3 + apps/browser/src/_locales/en_GB/messages.json | 3 + apps/browser/src/_locales/en_IN/messages.json | 3 + apps/browser/src/_locales/es/messages.json | 3 + apps/browser/src/_locales/et/messages.json | 3 + apps/browser/src/_locales/eu/messages.json | 3 + apps/browser/src/_locales/fa/messages.json | 3 + apps/browser/src/_locales/fi/messages.json | 3 + apps/browser/src/_locales/fil/messages.json | 3 + apps/browser/src/_locales/fr/messages.json | 3 + apps/browser/src/_locales/gl/messages.json | 3 + apps/browser/src/_locales/he/messages.json | 3 + apps/browser/src/_locales/hi/messages.json | 3 + apps/browser/src/_locales/hr/messages.json | 27 +++++---- apps/browser/src/_locales/hu/messages.json | 3 + apps/browser/src/_locales/id/messages.json | 3 + apps/browser/src/_locales/it/messages.json | 23 ++++---- apps/browser/src/_locales/ja/messages.json | 3 + apps/browser/src/_locales/ka/messages.json | 3 + apps/browser/src/_locales/km/messages.json | 3 + apps/browser/src/_locales/kn/messages.json | 3 + apps/browser/src/_locales/ko/messages.json | 41 +++++++------- apps/browser/src/_locales/lt/messages.json | 3 + apps/browser/src/_locales/lv/messages.json | 3 + apps/browser/src/_locales/ml/messages.json | 3 + apps/browser/src/_locales/mr/messages.json | 3 + apps/browser/src/_locales/my/messages.json | 3 + apps/browser/src/_locales/nb/messages.json | 3 + apps/browser/src/_locales/ne/messages.json | 3 + apps/browser/src/_locales/nl/messages.json | 3 + apps/browser/src/_locales/nn/messages.json | 3 + apps/browser/src/_locales/or/messages.json | 3 + apps/browser/src/_locales/pl/messages.json | 3 + apps/browser/src/_locales/pt_BR/messages.json | 3 + apps/browser/src/_locales/pt_PT/messages.json | 3 + apps/browser/src/_locales/ro/messages.json | 3 + apps/browser/src/_locales/ru/messages.json | 3 + apps/browser/src/_locales/si/messages.json | 3 + apps/browser/src/_locales/sk/messages.json | 3 + apps/browser/src/_locales/sl/messages.json | 3 + apps/browser/src/_locales/sr/messages.json | 3 + apps/browser/src/_locales/sv/messages.json | 3 + apps/browser/src/_locales/ta/messages.json | 3 + apps/browser/src/_locales/te/messages.json | 3 + apps/browser/src/_locales/th/messages.json | 3 + apps/browser/src/_locales/tr/messages.json | 3 + apps/browser/src/_locales/uk/messages.json | 3 + apps/browser/src/_locales/vi/messages.json | 3 + apps/browser/src/_locales/zh_CN/messages.json | 3 + apps/browser/src/_locales/zh_TW/messages.json | 5 +- 61 files changed, 252 insertions(+), 69 deletions(-) diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 10443fcf449..35d21b59be9 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index ad44440a343..2c9a496a95c 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Key Connector domenini təsdiqlə" }, + "atRiskLoginsSecured": { + "message": "Riskli girişlərinizi güvənli hala gətirməyiniz əladır!" + }, "settingDisabledByPolicy": { "message": "Bu ayar, təşkilatınızın siyasəti tərəfindən sıradan çıxarılıb.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index e9ec6a06b8c..f9fd41cf6e7 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 942a2f489a0..d8c288d9fca 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Потвърждаване на домейна на конектора за ключове" }, + "atRiskLoginsSecured": { + "message": "Добра работа с подсигуряването на данните за вписване в риск!" + }, "settingDisabledByPolicy": { "message": "Тази настройка е изключена съгласно политиката на организацията Ви.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index 8b8b89d45a2..1b8c289f717 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -23,7 +23,7 @@ "message": "অ্যাকাউন্ট তৈরি করুন" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "বিটওয়ার্ডেনে নতুন?" }, "logInWithPasskey": { "message": "Log in with passkey" @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 914b8700d13..8cc0d947199 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index 8f3a0ca386d..4483967ab33 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -320,7 +320,7 @@ "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." }, "twoStepLogin": { - "message": "Inici de sessió en dues passes" + "message": "Inici de sessió en dos passos" }, "logOut": { "message": "Tanca la sessió" @@ -970,7 +970,7 @@ "message": "Carpeta afegida" }, "twoStepLoginConfirmation": { - "message": "L'inici de sessió en dues passes fa que el vostre compte siga més segur, ja que obliga a verificar el vostre inici de sessió amb un altre dispositiu, com ara una clau de seguretat, una aplicació autenticadora, un SMS, una trucada telefònica o un correu electrònic. Es pot habilitar l'inici de sessió en dues passes a la caixa forta web de bitwarden.com. Voleu visitar el lloc web ara?" + "message": "L'inici de sessió en dos passos fa que el vostre compte siga més segur, ja que obliga a verificar el vostre inici de sessió amb un altre dispositiu, com ara una clau de seguretat, una aplicació autenticadora, un SMS, una trucada telefònica o un correu electrònic. Es pot habilitar l'inici de sessió en dos passos a la caixa forta web de bitwarden.com. Voleu visitar el lloc web ara?" }, "twoStepLoginConfirmationContent": { "message": "Fes que el vostre compte siga més segur configurant l'inici de sessió en dos passos a l'aplicació web de Bitwarden." @@ -1564,13 +1564,13 @@ "message": "Inici de sessió no disponible" }, "noTwoStepProviders": { - "message": "Aquest compte té habilitat l'inici de sessió en dues passes, però aquest navegador web no admet cap dels dos proveïdors configurats." + "message": "Aquest compte té habilitat l'inici de sessió en dos passos, però aquest navegador web no admet cap dels dos proveïdors configurats." }, "noTwoStepProviders2": { "message": "Utilitzeu un navegador web compatible (com ara Chrome) o afegiu proveïdors addicionals que siguen compatibles amb tots els navegadors web (com una aplicació d'autenticació)." }, "twoStepOptions": { - "message": "Opcions d'inici de sessió en dues passes" + "message": "Opcions d'inici de sessió en dos passos" }, "selectTwoStepLoginMethod": { "message": "Select two-step login method" @@ -1659,13 +1659,13 @@ "message": "Suggeriments d'emplenament automàtic" }, "autofillSpotlightTitle": { - "message": "Easily find autofill suggestions" + "message": "Trobeu fàcilment suggeriments d'emplenament automàtic" }, "autofillSpotlightDesc": { - "message": "Turn off your browser's autofill settings, so they don't conflict with Bitwarden." + "message": "Desactiveu la configuració d'emplenament automàtic del vostre navegador perquè no entren en conflicte amb Bitwarden." }, "turnOffBrowserAutofill": { - "message": "Turn off $BROWSER$ autofill", + "message": "Desactiveu l'emplenament automàtic de $BROWSER$", "placeholders": { "browser": { "content": "$1", @@ -1805,7 +1805,7 @@ "message": "Si feu clic a l'exterior de la finestra emergent per comprovar el vostre correu electrònic amb el codi de verificació, es tancarà aquesta finestra. Voleu obrir aquesta finestra emergent en una finestra nova perquè no es tanque?" }, "showIconsChangePasswordUrls": { - "message": "Show website icons and retrieve change password URLs" + "message": "Mostra les icones del lloc web i recupera els URL de canvi de contrasenya" }, "cardholderName": { "message": "Nom del titular de la targeta" @@ -3681,7 +3681,7 @@ "message": "Remember this device to make future logins seamless" }, "manageDevices": { - "message": "Manage devices" + "message": "Gestiona els dispositius" }, "currentSession": { "message": "Current session" @@ -3724,7 +3724,7 @@ "message": "Needs approval" }, "devices": { - "message": "Devices" + "message": "Dispositius" }, "accessAttemptBy": { "message": "Access attempt by $EMAIL$", @@ -4813,22 +4813,22 @@ "message": "Download Bitwarden" }, "downloadBitwardenOnAllDevices": { - "message": "Download Bitwarden on all devices" + "message": "Baixa Bitwarden a tots els dispositius" }, "getTheMobileApp": { "message": "Get the mobile app" }, "getTheMobileAppDesc": { - "message": "Access your passwords on the go with the Bitwarden mobile app." + "message": "Accediu a les vostres contrasenyes des de qualsevol lloc amb l'aplicació mòbil Bitwarden." }, "getTheDesktopApp": { - "message": "Get the desktop app" + "message": "Obteniu l'aplicació d'escriptori" }, "getTheDesktopAppDesc": { - "message": "Access your vault without a browser, then set up unlock with biometrics to expedite unlocking in both the desktop app and browser extension." + "message": "Accediu a la vostra caixa forta sense navegador i, a continuació, configureu el desbloqueig amb biometria per accelerar el desbloqueig tant a l'aplicació d'escriptori com a l'extensió del navegador." }, "downloadFromBitwardenNow": { - "message": "Download from bitwarden.com now" + "message": "Baixeu ara des de bitwarden.com" }, "getItOnGooglePlay": { "message": "Get it on Google Play" @@ -5298,10 +5298,10 @@ "message": "Biometric unlock is currently unavailable for an unknown reason." }, "unlockVault": { - "message": "Unlock your vault in seconds" + "message": "Desbloqueja la caixa forta en segons" }, "unlockVaultDesc": { - "message": "You can customize your unlock and timeout settings to more quickly access your vault." + "message": "Podeu personalitzar la configuració de desbloqueig i temps d'espera per accedir més ràpidament a la vostra caixa forta." }, "unlockPinSet": { "message": "Unlock PIN set" @@ -5538,7 +5538,7 @@ "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." }, "introCarouselLabel": { - "message": "Welcome to Bitwarden" + "message": "Benvinguts a Bitwarden" }, "securityPrioritized": { "message": "Security, prioritized" @@ -5577,7 +5577,7 @@ "message": "Import now" }, "hasItemsVaultNudgeTitle": { - "message": "Welcome to your vault!" + "message": "Benvigut/da a la vostra caixa forta!" }, "phishingPageTitleV2": { "message": "Phishing attempt detected" @@ -5612,13 +5612,13 @@ } }, "hasItemsVaultNudgeBodyOne": { - "message": "Autofill items for the current page" + "message": "Emplena automàticament els elements de la pàgina actual" }, "hasItemsVaultNudgeBodyTwo": { - "message": "Favorite items for easy access" + "message": "Elements preferits per accedir fàcilment" }, "hasItemsVaultNudgeBodyThree": { - "message": "Search your vault for something else" + "message": "Cerca altres coses a la caixa forta" }, "newLoginNudgeTitle": { "message": "Save time with autofill" @@ -5670,20 +5670,20 @@ "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, "generatorNudgeTitle": { - "message": "Quickly create passwords" + "message": "Creeu contrasenyes ràpidament" }, "generatorNudgeBodyOne": { - "message": "Easily create strong and unique passwords by clicking on", + "message": "Creeu fàcilment contrasenyes fortes i úniques fent clic a", "description": "Two part message", "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." }, "generatorNudgeBodyTwo": { - "message": "to help you keep your logins secure.", + "message": "per ajudar-vos a mantenir segurs els vostres inicis de sessió.", "description": "Two part message", "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." }, "generatorNudgeBodyAria": { - "message": "Easily create strong and unique passwords by clicking on the Generate password button to help you keep your logins secure.", + "message": "Creeu fàcilment contrasenyes fortes i úniques fent clic al botó Genera contrasenya per ajudar-vos a mantenir segurs els vostres inicis de sessió.", "description": "Aria label for the body content of the generator nudge" }, "aboutThisSetting": { @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index db522f3aa4e..b9383416eb4 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Potvrdit doménu Key Connectoru" }, + "atRiskLoginsSecured": { + "message": "Skvělá práce při zabezpečení přihlašovacích údajů v ohrožení!" + }, "settingDisabledByPolicy": { "message": "Toto nastavení je zakázáno zásadami Vaší organizace.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index eacbb06fd53..c18633c281c 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index ddc6f33599f..0f92552c9c1 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 8878f4b698e..411b73be447 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Key Connector-Domain bestätigen" }, + "atRiskLoginsSecured": { + "message": "Gute Arbeit! Du hast deine gefährdeten Zugangsdaten geschützt!" + }, "settingDisabledByPolicy": { "message": "Diese Einstellung ist durch die Richtlinien deiner Organisation deaktiviert.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 7f519130df0..025a66c5cde 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index 1b78e39ecf8..7fd3091ef75 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organisation's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index e7c3a197c75..88b95533ff1 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organisation's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index cc3242fd4a9..2adf87d63f3 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 96adaeba324..1500e20e3aa 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index ab1d3f8ef8e..81106464f69 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index 8d76ec3f428..6617ad085cc 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index b782c7e11af..57a6ecfedd0 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 6b85bdf8f43..88b94d9b9c1 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index a4518b54afc..15d1cdecacf 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirmez le domaine de Key Connector" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 66f459d97b7..137576cfb1f 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index 5243fa03283..2164d197b0e 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "אשר דומיין של Key Connector" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index e887f573ba9..bc36073156b 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index a4441a9a142..e678f506387 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -559,7 +559,7 @@ "description": "Verb" }, "unArchive": { - "message": "Unarchive" + "message": "Poništi arhiviranje" }, "itemsInArchive": { "message": "Stavke u arhivi" @@ -571,10 +571,10 @@ "message": "Arhivirane stavke biti će prikazane ovdje i biti će izuzete iz rezultata općih pretraga i preporuka auto-ispune." }, "itemWasSentToArchive": { - "message": "Item was sent to archive" + "message": "Stavka poslana u arhivu" }, "itemUnarchived": { - "message": "Item was unarchived" + "message": "Stavka vraćena iz arhive" }, "archiveItem": { "message": "Arhiviraj stavku" @@ -5580,30 +5580,30 @@ "message": "Dobrodošli u svoj trezor!" }, "phishingPageTitleV2": { - "message": "Phishing attempt detected" + "message": "Otkriven pokušaj phishinga" }, "phishingPageSummary": { - "message": "The site you are attempting to visit is a known malicious site and a security risk." + "message": "Web-mjesto koje pokušavaš posjetiti poznato je kao zlonamjerno i predstavlja sigurnosni rizik." }, "phishingPageCloseTabV2": { - "message": "Close this tab" + "message": "Zatvori ovu karticu" }, "phishingPageContinueV2": { - "message": "Continue to this site (not recommended)" + "message": "Nastavi na web mjesto (nije preporučljivo)" }, "phishingPageExplanation1": { - "message": "This site was found in ", + "message": "Ovo mjesto je nađeno na ", "description": "This is in multiple parts to allow for bold text in the middle of the sentence. A proper name follows this." }, "phishingPageExplanation2": { - "message": ", an open-source list of known phishing sites used for stealing personal and sensitive information.", + "message": ", popisu otvorenog koda poznatih phishing stranica koje se koriste za krađu osobnih i osjetljivih podataka.", "description": "This is in multiple parts to allow for bold text in the middle of the sentence. A proper name precedes this." }, "phishingPageLearnMore": { - "message": "Learn more about phishing detection" + "message": "Saznaj više o otkrivanju phishinga" }, "protectedBy": { - "message": "Protected by $PRODUCT$", + "message": "Zaštićeno s $PRODUCT$", "placeholders": { "product": { "content": "$1", @@ -5715,8 +5715,11 @@ "confirmKeyConnectorDomain": { "message": "Potvrdi domenu kontektora ključa" }, + "atRiskLoginsSecured": { + "message": "Rizične prijave su osigurane!" + }, "settingDisabledByPolicy": { - "message": "This setting is disabled by your organization's policy.", + "message": "Ova je postavka onemogućena pravilima tvoje organizacije.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index 2f3d46f78d8..e2674595f4b 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "A Key Connector tartomány megerősítése" }, + "atRiskLoginsSecured": { + "message": "Remek munka a kockázatos bejelentkezések biztosítása!" + }, "settingDisabledByPolicy": { "message": "Ezt a beállítást a szervezet házirendje letiltotta.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index ccd332b9c1b..a5757e38caf 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 10b1c678826..233ae413e5f 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -574,7 +574,7 @@ "message": "Elemento archiviato" }, "itemUnarchived": { - "message": "Item was unarchived" + "message": "Elemento rimosso dall'archivio" }, "archiveItem": { "message": "Archivia elemento" @@ -5580,30 +5580,30 @@ "message": "Benvenuto nella tua cassaforte!" }, "phishingPageTitleV2": { - "message": "Phishing attempt detected" + "message": "Tentativo di phishing rilevato" }, "phishingPageSummary": { - "message": "The site you are attempting to visit is a known malicious site and a security risk." + "message": "Stai cercando di visitare un sito dannoso noto che può mettere a rischio la tua sicurezza." }, "phishingPageCloseTabV2": { - "message": "Close this tab" + "message": "Chiudi tab" }, "phishingPageContinueV2": { - "message": "Continue to this site (not recommended)" + "message": "Vai al sito (SCONSIGLIATO!)" }, "phishingPageExplanation1": { - "message": "This site was found in ", + "message": "Questo sito è stato trovato in ", "description": "This is in multiple parts to allow for bold text in the middle of the sentence. A proper name follows this." }, "phishingPageExplanation2": { - "message": ", an open-source list of known phishing sites used for stealing personal and sensitive information.", + "message": ", un elenco open-source di siti di phishing noti per il furto di informazioni personali e sensibili.", "description": "This is in multiple parts to allow for bold text in the middle of the sentence. A proper name precedes this." }, "phishingPageLearnMore": { - "message": "Learn more about phishing detection" + "message": "Scopri di più sul rilevamento di phishing" }, "protectedBy": { - "message": "Protected by $PRODUCT$", + "message": "Protetto da $PRODUCT$", "placeholders": { "product": { "content": "$1", @@ -5715,8 +5715,11 @@ "confirmKeyConnectorDomain": { "message": "Conferma dominio Key Connector" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { - "message": "This setting is disabled by your organization's policy.", + "message": "Questa impostazione è disabilitata dalle restrizioni della tua organizzazione.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." } } diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 7c9d9e80ed4..4ab3cdc9c1b 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index 3b3189acd6d..4ea5ab3390a 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index 026e24dbd3a..e3e6953b0df 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 42a5c4f1b05..271db811810 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index b17055c72d0..c45532076da 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -189,7 +189,7 @@ "message": "노트 복사" }, "copy": { - "message": "Copy", + "message": "복사", "description": "Copy to clipboard" }, "fill": { @@ -383,7 +383,7 @@ "message": "폴더 편집" }, "editFolderWithName": { - "message": "Edit folder: $FOLDERNAME$", + "message": "폴더 편집: $FOLDERNAME$", "placeholders": { "foldername": { "content": "$1", @@ -471,10 +471,10 @@ "message": "패스프레이즈 생성됨" }, "usernameGenerated": { - "message": "Username generated" + "message": "사용자 이름 생성" }, "emailGenerated": { - "message": "Email generated" + "message": "이메일 생성" }, "regeneratePassword": { "message": "비밀번호 재생성" @@ -548,39 +548,39 @@ "message": "보관함 검색" }, "resetSearch": { - "message": "Reset search" + "message": "검색 초기화" }, "archiveNoun": { - "message": "Archive", + "message": "보관", "description": "Noun" }, "archiveVerb": { - "message": "Archive", + "message": "보관", "description": "Verb" }, "unArchive": { - "message": "Unarchive" + "message": "보관 해제" }, "itemsInArchive": { - "message": "Items in archive" + "message": "보관함의 항목" }, "noItemsInArchive": { - "message": "No items in archive" + "message": "보관함의 항목" }, "noItemsInArchiveDesc": { - "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + "message": "보관된 항목은 여기에 표시되며 일반 검색 결과 및 자동 완성 제안에서 제외됩니다." }, "itemWasSentToArchive": { - "message": "Item was sent to archive" + "message": "항목이 보관함으로 이동되었습니다" }, "itemUnarchived": { - "message": "Item was unarchived" + "message": "항목 보관 해제됨" }, "archiveItem": { - "message": "Archive item" + "message": "항목 보관" }, "archiveItemConfirmDesc": { - "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + "message": "보관된 항목은 일반 검색 결과와 자동 완성 제안에서 제외됩니다. 이 항목을 보관하시겠습니까?" }, "edit": { "message": "편집" @@ -589,7 +589,7 @@ "message": "보기" }, "viewLogin": { - "message": "View login" + "message": "로그인 보기" }, "noItemsInList": { "message": "항목이 없습니다." @@ -694,7 +694,7 @@ "message": "사용하고 있는 웹 브라우저가 쉬운 클립보드 복사를 지원하지 않습니다. 직접 복사하세요." }, "verifyYourIdentity": { - "message": "Verify your identity" + "message": "신원을 인증하세요" }, "weDontRecognizeThisDevice": { "message": "We don't recognize this device. Enter the code sent to your email to verify your identity." @@ -1236,7 +1236,7 @@ "message": "웹사이트에서 변경 사항이 감지되면 로그인 비밀번호를 업데이트하라는 메시지를 표시합니다. 모든 로그인된 계정에 적용됩니다." }, "enableUsePasskeys": { - "message": "패스키를 저장 및 사용할지 묻기" + "message": "패스키 저장 및 사용 확인" }, "usePasskeysDesc": { "message": "보관함에 새 패스키를 저장하거나 로그인할지 물어봅니다. 모든 로그인된 계정에 적용됩니다." @@ -3881,7 +3881,7 @@ "message": "Trust user" }, "sendsTitleNoItems": { - "message": "Send sensitive information safely", + "message": "민감한 정보 안전하게 보내세요", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendsBodyNoItems": { @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 4811d7585ed..e97a1cafcf9 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index ce2ffa00c40..e1189450671 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Apstiprināt Key Connector domēnu" }, + "atRiskLoginsSecured": { + "message": "Labs darbs riskam pakļauto pieteikšanās vienumu drošības uzlabošanā!" + }, "settingDisabledByPolicy": { "message": "Šis iestatījums ir atspējots apvienības pamatnostādnēs.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index 4641fc0416b..fcf73a37e45 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index 40370d4b980..93f78303a5c 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index 026e24dbd3a..e3e6953b0df 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 7091c084082..66d1ce615e1 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index 026e24dbd3a..e3e6953b0df 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index 7d8760a8710..73b8afa2966 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Key Connector-domein bevestigen" }, + "atRiskLoginsSecured": { + "message": "Goed gedaan, je hebt je risicovolle inloggegevens verbeterd!" + }, "settingDisabledByPolicy": { "message": "Deze instelling is uitgeschakeld door het beleid van uw organisatie.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index 026e24dbd3a..e3e6953b0df 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index 026e24dbd3a..e3e6953b0df 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 13d00bc6f88..78fb5e832a6 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Potwierdź domenę Key Connector" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index b1a6bc73f63..e3a82f42ca7 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirmar domínio do Conector de Chave" }, + "atRiskLoginsSecured": { + "message": "Ótimo trabalho protegendo suas credenciais em risco!" + }, "settingDisabledByPolicy": { "message": "Essa configuração está desativada pela política da sua organização.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 54eff3eb2ed..db2eb776d7f 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirmar o domínio do Key Connector" }, + "atRiskLoginsSecured": { + "message": "Excelente trabalho ao proteger as suas credenciais em risco!" + }, "settingDisabledByPolicy": { "message": "Esta configuração está desativada pela política da sua organização.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 9c1e2bcd79a..4b2913ce55b 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index 1100c4b382c..8661d78552e 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Подтвердите домен соединителя ключей" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "Этот параметр отключен политикой вашей организации.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index 76d4464489b..649556ca64b 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index 0d10ec1dd6b..fe86ad298c9 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Potvrdiť doménu Key Connectora" }, + "atRiskLoginsSecured": { + "message": "Skvelá práca pri zabezpečení vašich ohrozených prihlasovacích údajov!" + }, "settingDisabledByPolicy": { "message": "Politika organizácie vypla toto nastavenie.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 923fd2ce058..cd7eda9a4fa 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index 0cd98548b0f..3421dc1fae1 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Потврдите домен конектора кључа" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index e5de8bb5edf..07ed7a491f1 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Bekräfta Key Connector-domän" }, + "atRiskLoginsSecured": { + "message": "Bra jobbat med att säkra upp dina inloggninar i riskzonen!" + }, "settingDisabledByPolicy": { "message": "Denna inställning är inaktiverad enligt din organisations policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/ta/messages.json b/apps/browser/src/_locales/ta/messages.json index 68ae29a7a93..c4f0fffd143 100644 --- a/apps/browser/src/_locales/ta/messages.json +++ b/apps/browser/src/_locales/ta/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Key Connector டொமைனை உறுதிப்படுத்து" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index 026e24dbd3a..e3e6953b0df 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index cfb23d95a02..7487dea84bd 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Confirm Key Connector domain" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 206c0da5b88..e33addd805c 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Key Connector alan adını doğrulayın" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index 1adbf19496b..dba38faaec6 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Підтвердити домен Key Connector" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 885fe83f667..055e5155955 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "Xác nhận tên miền Key Connector" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index 738e2c13ecb..1d1a6674e18 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "确认 Key Connector 域名" }, + "atRiskLoginsSecured": { + "message": "Great job securing your at-risk logins!" + }, "settingDisabledByPolicy": { "message": "此设置被您组织的策略禁用了。", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index b83e78a3b02..e2d9ff2068f 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -6,7 +6,7 @@ "message": "Bitwarden logo" }, "extName": { - "message": "Bitwarden - 密碼管理工具", + "message": "Bitwarden 密碼管理器", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { @@ -5715,6 +5715,9 @@ "confirmKeyConnectorDomain": { "message": "確認 Key Connector 網域" }, + "atRiskLoginsSecured": { + "message": "你已成功保護有風險的登入項目,做得好!" + }, "settingDisabledByPolicy": { "message": "此設定已被你的組織原則停用。", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." From 6fdeefef3df0e453df7469ea39bd4517debb83ac Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Fri, 24 Oct 2025 08:54:37 +0200 Subject: [PATCH 24/73] Autosync the updated translations (#17011) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/af/messages.json | 8 ++++- apps/desktop/src/locales/ar/messages.json | 8 ++++- apps/desktop/src/locales/az/messages.json | 8 ++++- apps/desktop/src/locales/be/messages.json | 8 ++++- apps/desktop/src/locales/bg/messages.json | 8 ++++- apps/desktop/src/locales/bn/messages.json | 8 ++++- apps/desktop/src/locales/bs/messages.json | 8 ++++- apps/desktop/src/locales/ca/messages.json | 32 ++++++++++++-------- apps/desktop/src/locales/cs/messages.json | 8 ++++- apps/desktop/src/locales/cy/messages.json | 8 ++++- apps/desktop/src/locales/da/messages.json | 8 ++++- apps/desktop/src/locales/de/messages.json | 8 ++++- apps/desktop/src/locales/el/messages.json | 8 ++++- apps/desktop/src/locales/en_GB/messages.json | 8 ++++- apps/desktop/src/locales/en_IN/messages.json | 8 ++++- apps/desktop/src/locales/eo/messages.json | 8 ++++- apps/desktop/src/locales/es/messages.json | 8 ++++- apps/desktop/src/locales/et/messages.json | 8 ++++- apps/desktop/src/locales/eu/messages.json | 8 ++++- apps/desktop/src/locales/fa/messages.json | 8 ++++- apps/desktop/src/locales/fi/messages.json | 8 ++++- apps/desktop/src/locales/fil/messages.json | 8 ++++- apps/desktop/src/locales/fr/messages.json | 8 ++++- apps/desktop/src/locales/gl/messages.json | 8 ++++- apps/desktop/src/locales/he/messages.json | 8 ++++- apps/desktop/src/locales/hi/messages.json | 8 ++++- apps/desktop/src/locales/hr/messages.json | 16 +++++++--- apps/desktop/src/locales/hu/messages.json | 8 ++++- apps/desktop/src/locales/id/messages.json | 8 ++++- apps/desktop/src/locales/it/messages.json | 8 ++++- apps/desktop/src/locales/ja/messages.json | 8 ++++- apps/desktop/src/locales/ka/messages.json | 8 ++++- apps/desktop/src/locales/km/messages.json | 8 ++++- apps/desktop/src/locales/kn/messages.json | 8 ++++- apps/desktop/src/locales/ko/messages.json | 8 ++++- apps/desktop/src/locales/lt/messages.json | 8 ++++- apps/desktop/src/locales/lv/messages.json | 12 ++++++-- apps/desktop/src/locales/me/messages.json | 8 ++++- apps/desktop/src/locales/ml/messages.json | 8 ++++- apps/desktop/src/locales/mr/messages.json | 8 ++++- apps/desktop/src/locales/my/messages.json | 8 ++++- apps/desktop/src/locales/nb/messages.json | 8 ++++- apps/desktop/src/locales/ne/messages.json | 8 ++++- apps/desktop/src/locales/nl/messages.json | 8 ++++- apps/desktop/src/locales/nn/messages.json | 8 ++++- apps/desktop/src/locales/or/messages.json | 8 ++++- apps/desktop/src/locales/pl/messages.json | 8 ++++- apps/desktop/src/locales/pt_BR/messages.json | 8 ++++- apps/desktop/src/locales/pt_PT/messages.json | 8 ++++- apps/desktop/src/locales/ro/messages.json | 8 ++++- apps/desktop/src/locales/ru/messages.json | 8 ++++- apps/desktop/src/locales/si/messages.json | 8 ++++- apps/desktop/src/locales/sk/messages.json | 8 ++++- apps/desktop/src/locales/sl/messages.json | 8 ++++- apps/desktop/src/locales/sr/messages.json | 10 ++++-- apps/desktop/src/locales/sv/messages.json | 8 ++++- apps/desktop/src/locales/ta/messages.json | 8 ++++- apps/desktop/src/locales/te/messages.json | 8 ++++- apps/desktop/src/locales/th/messages.json | 8 ++++- apps/desktop/src/locales/tr/messages.json | 8 ++++- apps/desktop/src/locales/uk/messages.json | 8 ++++- apps/desktop/src/locales/vi/messages.json | 10 ++++-- apps/desktop/src/locales/zh_CN/messages.json | 8 ++++- apps/desktop/src/locales/zh_TW/messages.json | 8 ++++- 64 files changed, 468 insertions(+), 84 deletions(-) diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index 61927009c3a..0701eb833da 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Skrap rekening" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index aa72960577e..7e7f9faf5fe 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "قفل مع كلمة المرور الرئيسية عند إعادة تشغيل" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "حذف الحساب" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index 3bcd3f5f92b..4289d577aac 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Yenidən başladılanda ana parol ilə kilidlə" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Tətbiq yenidən başladıqda ana parol və ya PIN tələb edilsin" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Tətbiq yenidən başladıqda ana parol tələb edilsin" + }, "deleteAccount": { "message": "Hesabı sil" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Element arxivə göndərildi" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Element arxivdən çıxarıldı" }, "archiveItem": { diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index 981f042352d..1f2ac683790 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Выдаліць уліковы запіс" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index 64232779cc6..995d992db3e 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Заключване с главната парола при повторно пускане" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Изискване на главната парола или ПИН код при повторно пускане на приложението" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Изискване на главната парола при повторно пускане на приложението" + }, "deleteAccount": { "message": "Изтриване на регистрацията" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Елементът беше преместен в архива" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Елементът беше изваден от архива" }, "archiveItem": { diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index c33565740e2..7a64fec30da 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Delete account" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index 0a7061ba291..150b579b09d 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Brisanje računa" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index 58c2773f10a..dec07e3efe0 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -1009,16 +1009,16 @@ "message": "Inici de sessió no disponible" }, "noTwoStepProviders": { - "message": "Aquest compte té habilitat l'inici de sessió en dues passes, però aquest navegador web no admet cap dels dos proveïdors configurats." + "message": "Aquest compte té habilitat l'inici de sessió en dos passos, però aquest navegador web no admet cap dels dos proveïdors configurats." }, "noTwoStepProviders2": { "message": "Afegiu proveïdors addicionals que s'adapten millor als dispositius (com ara una aplicació d'autenticació)." }, "twoStepOptions": { - "message": "Opcions d'inici de sessió en dues passes" + "message": "Opcions d'inici de sessió en dos passos" }, "selectTwoStepLoginMethod": { - "message": "Seleccioneu un mètode d'inici de sessió en dues passes" + "message": "Seleccioneu un mètode d'inici de sessió en dos passos" }, "selfHostedEnvironment": { "message": "Entorn d'allotjament propi" @@ -1232,13 +1232,13 @@ } }, "twoStepLoginConfirmation": { - "message": "L'inici de sessió en dues passes fa que el vostre compte siga més segur, ja que obliga a comprovar el vostre inici de sessió amb un altre dispositiu, com ara una clau de seguretat, una aplicació autenticadora, un SMS, una trucada telefònica o un correu electrònic. Es pot habilitar l'inici de sessió en dues passes a la caixa forta web de bitwarden.com. Voleu visitar el lloc web ara?" + "message": "L'inici de sessió en dos passos fa que el vostre compte siga més segur, ja que obliga a comprovar el vostre inici de sessió amb un altre dispositiu, com ara una clau de seguretat, una aplicació autenticadora, un SMS, una trucada telefònica o un correu electrònic. Es pot habilitar l'inici de sessió en dos passos a la caixa forta web de bitwarden.com. Voleu visitar el lloc web ara?" }, "twoStepLogin": { - "message": "Inici de sessió en dues passes" + "message": "Inici de sessió en dos passos" }, "vaultTimeoutHeader": { - "message": "Vault timeout" + "message": "Temps d'espera de la caixa forta" }, "vaultTimeout": { "message": "Temps d'espera de la caixa forta" @@ -1247,7 +1247,7 @@ "message": "Temps d'espera" }, "vaultTimeoutAction1": { - "message": "Timeout action" + "message": "Acció després del temps d'espera" }, "vaultTimeoutDesc": { "message": "Trieu quan es tancarà la vostra caixa forta i feu l'acció seleccionada." @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Bloqueja amb la contrasenya mestra en reiniciar" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Suprimeix el compte" }, @@ -1999,7 +2005,7 @@ } }, "learnMoreAboutAuthenticators": { - "message": "Learn more about authenticators" + "message": "Més informació sobre els autenticadors" }, "copyTOTP": { "message": "Copy Authenticator key (TOTP)" @@ -2118,7 +2124,7 @@ "message": "Habilita la integració amb el navegador" }, "enableBrowserIntegrationDesc1": { - "message": "Used to allow biometric unlock in browsers that are not Safari." + "message": "Es fa servir per permetre el desbloqueig biomètric en navegadors que no són Safari." }, "enableDuckDuckGoBrowserIntegration": { "message": "Permet la integració del navegador DuckDuckGo" @@ -3952,15 +3958,15 @@ "message": "With notes, securely store sensitive data like banking or insurance details." }, "newSshNudgeTitle": { - "message": "Developer-friendly SSH access" + "message": "Accés SSH fàcil per a desenvolupadors" }, "newSshNudgeBodyOne": { - "message": "Store your keys and connect with the SSH agent for fast, encrypted authentication.", + "message": "Emmagatzemeu les claus i connecteu-vos amb l'agent SSH per a una autenticació ràpida i xifrada.", "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, "newSshNudgeBodyTwo": { - "message": "Learn more about SSH agent", + "message": "Més informació sobre l'agent SSH", "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index 444d61c172a..50b9cdf8844 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Zamknout trezor při restartu pomocí hlavního hesla" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Vyžadovat hlavní heslo nebo PIN při restartu aplikace" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Vyžadovat hlavní heslo při restartu aplikace" + }, "deleteAccount": { "message": "Smazat účet" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Položka byla přesunuta do archivu" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Položka byla odebrána z archivu" }, "archiveItem": { diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index 5225818ec95..03a29097352 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Delete account" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index 33b333a61a1..a5a45db979a 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Lås med hovedadgangskode ved genstart" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Slet konto" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index 77052612eb4..002ef104b96 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Beim Neustart mit Master-Passwort sperren" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Master-Passwort oder PIN beim App-Neustart erfordern" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Master-Passwort beim App-Neustart erfordern" + }, "deleteAccount": { "message": "Konto löschen" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Eintrag wurde ins Archiv verschoben" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Eintrag wird nicht mehr archiviert" }, "archiveItem": { diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index 858dee3849e..6d381b8fa66 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Κλείδωμα με τον κύριο κωδικό πρόσβασης κατά την επανεκκίνηση" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Διαγραφή λογαριασμού" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index 5151cd2502d..6594b2812e3 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Delete account" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index fdc4537c1a6..20745ccfaf1 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Delete account" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index 828f82495b0..14972f29f79 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Ŝlosi per la ĉefa pasvorto ĉe relanĉo" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Forigi la konton" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index 0368ea0f202..2850044205f 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Bloquear con contraseña maestra al reiniciar" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Eliminar cuenta" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index e33fe78e56b..75395b451b6 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Lukusta ülemparooliga, kui rakendus taaskäivitatakse" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Kustuta konto" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index 9306b55ec8b..0f5ebaca284 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Ezabatu kontua" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index 4b1d32a2d7a..f097a21b7b7 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "در زمان شروع مجدد، با کلمه عبور اصلی قفل کن" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "حذف حساب کاربری" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index 3b54c4d0757..725f1ebb7f2 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Lukitse pääsalasanalla uudelleenkäynnistyksen yhteydessä" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Poista tili" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index 5334b43c35a..a23e6913c06 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Tanggalin ang account" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index 38edba7136a..10885ea46f4 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Verrouiller avec le mot de passe principal au redémarrage" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Supprimer le compte" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index be029fc4e2e..3073fef032a 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Delete account" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index cad04a51a9b..fcbd038adf3 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "נעל בעזרת הסיסמה הראשית בהפעלה מחדש" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "מחק חשבון" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index 8b00acfe49b..ca2b4cbced1 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Delete account" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index 8f21ddd199e..129dd27b09a 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Zaključaj glavnom lozinkom kod svakog pokretanja" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Traži glavnu lozinku ili PIN kod ponovnog pokretanja aplikacije" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Traži glavnu lozinku kod ponovnog pokretanja aplikacije" + }, "deleteAccount": { "message": "Obriši račun" }, @@ -2550,7 +2556,7 @@ } }, "vaultCustomTimeoutMinimum": { - "message": "Minimum custom timeout is 1 minute." + "message": "Najmanje prilagođeno vrijeme je 1 minuta." }, "inviteAccepted": { "message": "Pozivnica prihvaćena" @@ -4153,7 +4159,7 @@ "description": "Verb" }, "unArchive": { - "message": "Unarchive" + "message": "Poništi arhiviranje" }, "itemsInArchive": { "message": "Stavke u arhivi" @@ -4165,10 +4171,10 @@ "message": "Arhivirane stavke biti će prikazane ovdje i biti će izuzete iz rezultata općih pretraga i preporuka auto-ispune." }, "itemWasSentToArchive": { - "message": "Item was sent to archive" + "message": "Stavka poslana u arhivu" }, - "itemUnarchived": { - "message": "Item was unarchived" + "itemWasUnarchived": { + "message": "Stavka vraćena iz arhive" }, "archiveItem": { "message": "Arhiviraj stavku" diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index 7dd61d4fd28..8e06affda49 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Lezárás mesterjelszóval újraindításkor" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Mesterjelszó vagy PIN kód szükséges az alkalmazás indításakor." + }, + "requireMasterPasswordOnAppRestart": { + "message": "Mesterjelszó szükséges az alkalmazás indításakor." + }, "deleteAccount": { "message": "Fiók törlése" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Az elem az archivumba került." }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Az elem visszavéelre került az archivumból." }, "archiveItem": { diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index 73c09e2d972..2aea4e5f1ab 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Hapus akun" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index 1ef09903a83..c851dc2b298 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Blocca con password principale al riavvio" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Elimina account" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index 92e86c44f39..1b61929ac38 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "再起動時にマスターパスワードでロック" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "アカウントを削除" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index f5f21a5eec5..769cc602815 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "ანგარიშის წაშლა" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index be029fc4e2e..3073fef032a 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Delete account" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index 7281afada7f..e987d3d811b 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Delete account" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index 57af822737b..55da2761122 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "계정 삭제" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index f5f535aee09..38971c8c675 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Ištrinti paskyrą" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index e12458b1bc4..487991ddfa4 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Aizslēgt ar galveno paroli pēc pārsāknēšanas" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Pieprasīt galveno paroli vai PIN pēc lietotnes pārsāknēšanas" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Pieprasīt galveno paroli pēc lietotnes pārsāknēšanas" + }, "deleteAccount": { "message": "Izdzēst kontu" }, @@ -4145,11 +4151,11 @@ "message": "Labot saīsni" }, "archiveNoun": { - "message": "Archive", + "message": "Arhīvs", "description": "Noun" }, "archiveVerb": { - "message": "Archive", + "message": "Arhivēt", "description": "Verb" }, "unArchive": { @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Vienums tika ievietots arhīvā" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Vienums tika izņemts no arhīva" }, "archiveItem": { diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index 26957e97439..298d13ce2dd 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Delete account" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index e6b7dd42f2a..456ade5aec7 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Delete account" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index be029fc4e2e..3073fef032a 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Delete account" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index c230591d212..7d754800060 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Delete account" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index a4ee9d6fac5..7c70d751245 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Slett konto" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index 81e339d099a..bf78d49ff23 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Delete account" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index 3330f148c0e..fd568c9bbb4 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Bij herstart vergrendelen met hoofdwachtwoord" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Hoofdwachtwoord of pincode vereisen bij herstart van de app" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Hoofdwachtwoord vereisen bij herstart van de app" + }, "deleteAccount": { "message": "Account verwijderen" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item naar archief verzonden" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item uit het archief gehaald" }, "archiveItem": { diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index 92caf401a11..bee0f8ed4fc 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Delete account" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index ffe8f673a1e..32c05bd53ff 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Delete account" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index 4abade3d1c8..08585674532 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Zablokuj hasłem głównym po uruchomieniu ponownym" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Usuń konto" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Element został przeniesiony do archiwum" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Element został usunięty z archiwum" }, "archiveItem": { diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index 4c09f4f7cfe..4e80920cbf9 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Bloquear com senha mestra ao reiniciar" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Exigir senha mestra ou PIN ao reiniciar o app" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Exigir senha mestra ao reiniciar o app" + }, "deleteAccount": { "message": "Apagar conta" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "O item foi enviado para o arquivo" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "O item foi desarquivado" }, "archiveItem": { diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index 716d0089506..0bb2142ba5a 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Bloquear com a palavra-passe mestra ao reiniciar" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Exigir a palavra-passe mestra ou PIN ao reiniciar a app" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Exigir a palavra-passe mestra ao reiniciar a app" + }, "deleteAccount": { "message": "Eliminar conta" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "O item foi movido para o arquivo" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "O item foi desarquivado" }, "archiveItem": { diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index 9231fe210b2..dab5ed8112e 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Ștergere cont" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index 5d116cda009..684adf61875 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Блокировать мастер-паролем при перезапуске" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Требовать мастер-пароль или PIN при перезапуске приложения" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Требовать мастер-пароль при перезапуске приложения" + }, "deleteAccount": { "message": "Удалить аккаунт" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Элемент был отправлен в архив" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Элемент был разархивирован" }, "archiveItem": { diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index ece911b848b..397bbbe23c7 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Delete account" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index 9c98013eb67..66a42c52182 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Pri reštarte zamknúť hlavným heslom" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Pri reštarte aplikácie vyžadovať hlavné heslo alebo PIN" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Pri reštarte aplikácie vyžadovať hlavné heslo" + }, "deleteAccount": { "message": "Odstrániť účet" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Položka bola archivovaná" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Položka bola odobraná z archívu" }, "archiveItem": { diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index e63d37a92c6..597cb62b4ea 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Delete account" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index bb2fae4e2a3..20e55677171 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Закључајте са главном лозинком при поновном покретању" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Брисање налога" }, @@ -4167,8 +4173,8 @@ "itemWasSentToArchive": { "message": "Ставка је послата у архиву" }, - "itemUnarchived": { - "message": "Ставка враћена из архиве" + "itemWasUnarchived": { + "message": "Item was unarchived" }, "archiveItem": { "message": "Архивирај ставку" diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index 4e9133cd6f8..575e5755441 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Lås med huvudlösenord vid omstart" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Kräv huvudlösenord eller PIN-kod vid omstart av appen" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Kräv huvudlösenord vid omstart av appen" + }, "deleteAccount": { "message": "Radera konto" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Objektet skickades till arkivet" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Objektet har avarkiverats" }, "archiveItem": { diff --git a/apps/desktop/src/locales/ta/messages.json b/apps/desktop/src/locales/ta/messages.json index ed4e61b04f8..0ee270c981b 100644 --- a/apps/desktop/src/locales/ta/messages.json +++ b/apps/desktop/src/locales/ta/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "மறுதொடக்கம் செய்யும் போது முதன்மை கடவுச்சொல்லுடன் பூட்டவும்" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "கணக்கை நீக்கவும்" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index be029fc4e2e..3073fef032a 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Delete account" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index 60362ce09a6..a2637894dc4 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Lock with master password on restart" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "ลบบัญชีผู้ใช้" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index 54fb09d147e..0d93d84fa2a 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Yeniden başlatmada ana parola ile kilitle" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Uygulama yeniden başlatıldığında ana parola veya PIN iste" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Uygulama yeniden başlatıldığında ana parola iste" + }, "deleteAccount": { "message": "Hesabı sil" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Kayıt arşive gönderildi" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Kayıt arşivden çıkarıldı" }, "archiveItem": { diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index 3f80e8c1e9e..577ce4a5d78 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Блокувати головним паролем при перезапуску" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Видалити обліковий запис" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "Item was sent to archive" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "Item was unarchived" }, "archiveItem": { diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index 51274907720..f1341734453 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "Khóa bằng mật khẩu chính khi khởi động lại" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "Require master password or PIN on app restart" + }, + "requireMasterPasswordOnAppRestart": { + "message": "Require master password on app restart" + }, "deleteAccount": { "message": "Xóa tài khoản" }, @@ -4167,8 +4173,8 @@ "itemWasSentToArchive": { "message": "Mục đã được chuyển vào kho lưu trữ" }, - "itemUnarchived": { - "message": "Mục đã được bỏ lưu trữ" + "itemWasUnarchived": { + "message": "Item was unarchived" }, "archiveItem": { "message": "Lưu trữ mục" diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index d3ec23a7994..9a8aa724778 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "重启后使用主密码锁定" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "App 重启时要求主密码或 PIN 码" + }, + "requireMasterPasswordOnAppRestart": { + "message": "App 重启时要求主密码" + }, "deleteAccount": { "message": "删除账户" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "项目已发送到归档" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "项目已取消归档" }, "archiveItem": { diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index f22651b960b..9b29eb12a2d 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -1852,6 +1852,12 @@ "lockWithMasterPassOnRestart1": { "message": "重啟後使用主密碼鎖定" }, + "requireMasterPasswordOrPinOnAppRestart": { + "message": "要求在重新啟動應用程式時輸入密碼或 PIN 碼" + }, + "requireMasterPasswordOnAppRestart": { + "message": "在應用程式重啟時重新詢問主密碼" + }, "deleteAccount": { "message": "刪除帳戶" }, @@ -4167,7 +4173,7 @@ "itemWasSentToArchive": { "message": "項目已移至封存" }, - "itemUnarchived": { + "itemWasUnarchived": { "message": "項目取消封存" }, "archiveItem": { From 2d3712acec8e00f4edb11133010cc87192529cef Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Fri, 24 Oct 2025 11:48:05 +0100 Subject: [PATCH 25/73] [PM-27257]Fix : Remove Welcome to Bitwarden modal for users with any Organization status (#17002) * Resolve the modal for invited members * Resolve multiple modal display * Fix the failing test * Remove the await --- .../unified-upgrade-prompt.service.spec.ts | 30 ++++++++++++++++ .../unified-upgrade-prompt.service.ts | 36 +++++++++++++++++-- .../vault/individual-vault/vault.component.ts | 2 +- 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/apps/web/src/app/billing/individual/upgrade/services/unified-upgrade-prompt.service.spec.ts b/apps/web/src/app/billing/individual/upgrade/services/unified-upgrade-prompt.service.spec.ts index a9133d220c3..a0b71e598f6 100644 --- a/apps/web/src/app/billing/individual/upgrade/services/unified-upgrade-prompt.service.spec.ts +++ b/apps/web/src/app/billing/individual/upgrade/services/unified-upgrade-prompt.service.spec.ts @@ -3,10 +3,12 @@ import * as rxjs from "rxjs"; import { of } from "rxjs"; import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService, Account } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { SyncService } from "@bitwarden/common/platform/sync/sync.service"; import { DialogRef, DialogService } from "@bitwarden/components"; import { @@ -22,7 +24,9 @@ describe("UnifiedUpgradePromptService", () => { const mockConfigService = mock(); const mockBillingService = mock(); const mockVaultProfileService = mock(); + const mockSyncService = mock(); const mockDialogService = mock(); + const mockOrganizationService = mock(); const mockDialogOpen = jest.spyOn(UnifiedUpgradeDialogComponent, "open"); /** @@ -50,7 +54,9 @@ describe("UnifiedUpgradePromptService", () => { mockConfigService, mockBillingService, mockVaultProfileService, + mockSyncService, mockDialogService, + mockOrganizationService, ); } @@ -81,6 +87,12 @@ describe("UnifiedUpgradePromptService", () => { mockReset(mockConfigService); mockReset(mockBillingService); mockReset(mockVaultProfileService); + mockReset(mockSyncService); + mockReset(mockOrganizationService); + + // Mock sync service methods + mockSyncService.fullSync.mockResolvedValue(true); + mockSyncService.lastSync$.mockReturnValue(of(new Date())); }); it("should not show dialog when feature flag is disabled", async () => { // Arrange @@ -97,6 +109,21 @@ describe("UnifiedUpgradePromptService", () => { // Arrange mockConfigService.getFeatureFlag$.mockReturnValue(of(true)); mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(true)); + mockOrganizationService.memberOrganizations$.mockReturnValue(of([])); + setupTestService(); + + // Act + const result = await sut.displayUpgradePromptConditionally(); + + // Assert + expect(result).toBeNull(); + }); + + it("should not show dialog when user has any organization membership", async () => { + // Arrange + mockConfigService.getFeatureFlag$.mockReturnValue(of(true)); + mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(false)); + mockOrganizationService.memberOrganizations$.mockReturnValue(of([{ id: "org1" } as any])); setupTestService(); // Act @@ -110,6 +137,7 @@ describe("UnifiedUpgradePromptService", () => { // Arrange mockConfigService.getFeatureFlag$.mockReturnValue(of(true)); mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(false)); + mockOrganizationService.memberOrganizations$.mockReturnValue(of([])); const oldDate = new Date(); oldDate.setMinutes(oldDate.getMinutes() - 10); // 10 minutes old mockVaultProfileService.getProfileCreationDate.mockResolvedValue(oldDate); @@ -126,6 +154,7 @@ describe("UnifiedUpgradePromptService", () => { //Arrange mockConfigService.getFeatureFlag$.mockReturnValue(of(true)); mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(false)); + mockOrganizationService.memberOrganizations$.mockReturnValue(of([])); const recentDate = new Date(); recentDate.setMinutes(recentDate.getMinutes() - 3); // 3 minutes old mockVaultProfileService.getProfileCreationDate.mockResolvedValue(recentDate); @@ -159,6 +188,7 @@ describe("UnifiedUpgradePromptService", () => { // Arrange mockConfigService.getFeatureFlag$.mockReturnValue(of(true)); mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(false)); + mockOrganizationService.memberOrganizations$.mockReturnValue(of([])); mockVaultProfileService.getProfileCreationDate.mockResolvedValue(null); setupTestService(); diff --git a/apps/web/src/app/billing/individual/upgrade/services/unified-upgrade-prompt.service.ts b/apps/web/src/app/billing/individual/upgrade/services/unified-upgrade-prompt.service.ts index e90f696cfb5..8dd7f31275c 100644 --- a/apps/web/src/app/billing/individual/upgrade/services/unified-upgrade-prompt.service.ts +++ b/apps/web/src/app/billing/individual/upgrade/services/unified-upgrade-prompt.service.ts @@ -1,12 +1,14 @@ import { Injectable } from "@angular/core"; -import { combineLatest, firstValueFrom } from "rxjs"; -import { switchMap, take } from "rxjs/operators"; +import { combineLatest, firstValueFrom, timeout } from "rxjs"; +import { filter, switchMap, take } from "rxjs/operators"; import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { SyncService } from "@bitwarden/common/platform/sync/sync.service"; import { DialogRef, DialogService } from "@bitwarden/components"; import { @@ -24,7 +26,9 @@ export class UnifiedUpgradePromptService { private configService: ConfigService, private billingAccountProfileStateService: BillingAccountProfileStateService, private vaultProfileService: VaultProfileService, + private syncService: SyncService, private dialogService: DialogService, + private organizationService: OrganizationService, ) {} private shouldShowPrompt$ = combineLatest([ @@ -40,6 +44,19 @@ export class UnifiedUpgradePromptService { return false; } + // Wait for sync to complete to ensure organizations are fully loaded + // Also force a sync to ensure we have the latest data + await this.syncService.fullSync(false); + + // Wait for the sync to complete with timeout to prevent hanging + await firstValueFrom( + this.syncService.lastSync$(account.id).pipe( + filter((lastSync) => lastSync !== null), + take(1), + timeout(30000), // 30 second timeout + ), + ); + // Check if user has premium const hasPremium = await firstValueFrom( this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), @@ -50,12 +67,25 @@ export class UnifiedUpgradePromptService { return false; } + // Check if user has any organization membership (any status including pending) + // Try using memberOrganizations$ which might have different filtering logic + const memberOrganizations = await firstValueFrom( + this.organizationService.memberOrganizations$(account.id), + ); + + const hasOrganizations = memberOrganizations.length > 0; + + // Early return if user has any organization status + if (hasOrganizations) { + return false; + } + // Check profile age only if needed const isProfileLessThanFiveMinutesOld = await this.isProfileLessThanFiveMinutesOld( account.id, ); - return isFlagEnabled && !hasPremium && isProfileLessThanFiveMinutesOld; + return isFlagEnabled && !hasPremium && !hasOrganizations && isProfileLessThanFiveMinutesOld; }), take(1), ); 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 32f35375542..7ea1d02110d 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -620,7 +620,7 @@ export class VaultComponent implements OnInit, OnDestr this.changeDetectorRef.markForCheck(); }, ); - await this.unifiedUpgradePromptService.displayUpgradePromptConditionally(); + void this.unifiedUpgradePromptService.displayUpgradePromptConditionally(); } ngOnDestroy() { From e8154cf5ad625ca9545f57c2e1f679a328bf698c Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Fri, 24 Oct 2025 13:16:22 +0200 Subject: [PATCH 26/73] Autosync the updated translations (#17013) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/src/locales/af/messages.json | 128 +++++++++++++--- apps/web/src/locales/ar/messages.json | 128 +++++++++++++--- apps/web/src/locales/az/messages.json | 128 +++++++++++++--- apps/web/src/locales/be/messages.json | 128 +++++++++++++--- apps/web/src/locales/bg/messages.json | 128 +++++++++++++--- apps/web/src/locales/bn/messages.json | 128 +++++++++++++--- apps/web/src/locales/bs/messages.json | 128 +++++++++++++--- apps/web/src/locales/ca/messages.json | 154 +++++++++++++++----- apps/web/src/locales/cs/messages.json | 128 +++++++++++++--- apps/web/src/locales/cy/messages.json | 128 +++++++++++++--- apps/web/src/locales/da/messages.json | 128 +++++++++++++--- apps/web/src/locales/de/messages.json | 132 +++++++++++++---- apps/web/src/locales/el/messages.json | 128 +++++++++++++--- apps/web/src/locales/en_GB/messages.json | 128 +++++++++++++--- apps/web/src/locales/en_IN/messages.json | 128 +++++++++++++--- apps/web/src/locales/eo/messages.json | 128 +++++++++++++--- apps/web/src/locales/es/messages.json | 128 +++++++++++++--- apps/web/src/locales/et/messages.json | 128 +++++++++++++--- apps/web/src/locales/eu/messages.json | 128 +++++++++++++--- apps/web/src/locales/fa/messages.json | 128 +++++++++++++--- apps/web/src/locales/fi/messages.json | 128 +++++++++++++--- apps/web/src/locales/fil/messages.json | 128 +++++++++++++--- apps/web/src/locales/fr/messages.json | 128 +++++++++++++--- apps/web/src/locales/gl/messages.json | 128 +++++++++++++--- apps/web/src/locales/he/messages.json | 128 +++++++++++++--- apps/web/src/locales/hi/messages.json | 128 +++++++++++++--- apps/web/src/locales/hr/messages.json | 164 +++++++++++++++------ apps/web/src/locales/hu/messages.json | 128 +++++++++++++--- apps/web/src/locales/id/messages.json | 128 +++++++++++++--- apps/web/src/locales/it/messages.json | 148 ++++++++++++++----- apps/web/src/locales/ja/messages.json | 128 +++++++++++++--- apps/web/src/locales/ka/messages.json | 128 +++++++++++++--- apps/web/src/locales/km/messages.json | 128 +++++++++++++--- apps/web/src/locales/kn/messages.json | 128 +++++++++++++--- apps/web/src/locales/ko/messages.json | 128 +++++++++++++--- apps/web/src/locales/lv/messages.json | 178 ++++++++++++++++------- apps/web/src/locales/ml/messages.json | 128 +++++++++++++--- apps/web/src/locales/mr/messages.json | 128 +++++++++++++--- apps/web/src/locales/my/messages.json | 128 +++++++++++++--- apps/web/src/locales/nb/messages.json | 128 +++++++++++++--- apps/web/src/locales/ne/messages.json | 128 +++++++++++++--- apps/web/src/locales/nl/messages.json | 128 +++++++++++++--- apps/web/src/locales/nn/messages.json | 128 +++++++++++++--- apps/web/src/locales/or/messages.json | 128 +++++++++++++--- apps/web/src/locales/pl/messages.json | 128 +++++++++++++--- apps/web/src/locales/pt_BR/messages.json | 128 +++++++++++++--- apps/web/src/locales/pt_PT/messages.json | 128 +++++++++++++--- apps/web/src/locales/ro/messages.json | 128 +++++++++++++--- apps/web/src/locales/ru/messages.json | 128 +++++++++++++--- apps/web/src/locales/si/messages.json | 128 +++++++++++++--- apps/web/src/locales/sk/messages.json | 166 +++++++++++++++------ apps/web/src/locales/sl/messages.json | 128 +++++++++++++--- apps/web/src/locales/sr_CS/messages.json | 128 +++++++++++++--- apps/web/src/locales/sr_CY/messages.json | 128 +++++++++++++--- apps/web/src/locales/sv/messages.json | 136 +++++++++++++---- apps/web/src/locales/ta/messages.json | 128 +++++++++++++--- apps/web/src/locales/te/messages.json | 128 +++++++++++++--- apps/web/src/locales/th/messages.json | 128 +++++++++++++--- apps/web/src/locales/tr/messages.json | 130 +++++++++++++---- apps/web/src/locales/uk/messages.json | 128 +++++++++++++--- apps/web/src/locales/vi/messages.json | 128 +++++++++++++--- apps/web/src/locales/zh_CN/messages.json | 130 +++++++++++++---- apps/web/src/locales/zh_TW/messages.json | 128 +++++++++++++--- 63 files changed, 6645 insertions(+), 1605 deletions(-) diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index 70cd9a860b5..065d91f727b 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Enkripsiesleutelinstellings" }, - "kdfAlgorithm": { - "message": "KDF-algoritme" - }, "kdfIterations": { "message": "KDF-iteraties" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Higher KDF iterations, memory, and parallelism can help protect your master password from being brute forced by an attacker." }, - "changeKdf": { - "message": "Verander KDF" - }, "encKeySettingsChanged": { "message": "Enkripsiesleutelinstellings is verander" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Persoonlike eienaarskap" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Off" }, - "higherKDFIterations": { - "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." - }, - "incrementsOf100,000": { - "message": "increments of 100,000" - }, - "smallIncrements": { - "message": "small increments" - }, "kdfIterationRecommends": { "message": "We recommend 600,000 or more" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "For older devices, setting your KDF too high may lead to performance issues. Increase the value in $VALUE$ and test your devices.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index 5e1f3e7997b..fa45c8ad898 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "الأعضاء المبلّغون ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "إعدادات مفتاح التشفير" }, - "kdfAlgorithm": { - "message": "خوارزمية KDF" - }, "kdfIterations": { "message": "تكرار KDF" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Higher KDF iterations, memory, and parallelism can help protect your master password from being brute forced by an attacker." }, - "changeKdf": { - "message": "تغيير KDF" - }, "encKeySettingsChanged": { "message": "تم حفظ إعدادات مفتاح التشفير" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Off" }, - "higherKDFIterations": { - "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." - }, - "incrementsOf100,000": { - "message": "increments of 100,000" - }, - "smallIncrements": { - "message": "small increments" - }, "kdfIterationRecommends": { "message": "We recommend 600,000 or more" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "For older devices, setting your KDF too high may lead to performance issues. Increase the value in $VALUE$ and test your devices.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index df5d51b12ec..0d4205ca5ab 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ yeni parol risklidir", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Məlumatlandırılan üzvlər ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Şifrələmə açarı ayarları" }, - "kdfAlgorithm": { - "message": "KDF Algoritmi" - }, "kdfIterations": { "message": "KDF iterasiyaları" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Yüksək KDF iterasiyaları, yaddaşı və paralelizmi, ana parolunuzu təcavüzkar tərəfindən \"brute force\" hücumuna qarşı qorumağa kömək edir." }, - "changeKdf": { - "message": "KDF-i dəyişdir" - }, "encKeySettingsChanged": { "message": "Şifrələmə açarı ayarları dəyişdirildi" }, @@ -5710,6 +5713,65 @@ "message": "Kimlik məlumatlarının dövriyyəsi barədə ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "İndi mövcuddur" + }, + "autoConfirm": { + "message": "Yeni istifadəçilər üçün avtomatik təsdiq" + }, + "autoConfirmDescription": { + "message": "Təşkilata dəvət edilmiş yeni istifadəçilər, admin cihazının kilidi açıldığı zaman avtomatik olaraq təsdiqlənəcək.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "Avtomatik istifadəçi təsdiqi necə işə salınır" + }, + "autoConfirmStep1": { + "message": "Bitwarden uzantınızı açın." + }, + "autoConfirmStep2a": { + "message": "Seçin", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " İşə sal.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Bitwarden brauzer uzantısı uğurla açıldı. Artıq avtomatik istifadəçi təsdiqi ayarını aktivləşdirə bilərsiniz." + }, + "autoConfirmPolicyEditDescription": { + "message": "Təşkilata dəvət edilmiş yeni istifadəçilər, admin cihazının kilidi açıldığı zaman avtomatik olaraq təsdiqlənəcək. Bu siyasəti işə salmazdan əvvəl, lütfən aşağıdakıları incələyin və razılaşın: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potensial təhlükəsizlik riski. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Avtomatik istifadəçi təsdiqi, təşkilatınızın veriləri üçün təhlükəsizlik riski yarada bilər." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Risklər barədə öyrən", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Vahid təşkilat siyasəti tələb olunur. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Birdən çox təşkilatın üzvü olan hər kəsin erişimi, digər təşkilatları tərk edənə qədər ləğv ediləcək." + }, + "autoConfirmSingleOrgExemption": { + "message": "Vahid təşkilat siyasəti bütün rollara şamil ediləcək. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "Fövqəladə hal erişimi yoxdur. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Fövqəladə Erişim silinib." + }, + "autoConfirmCheckBoxLabel": { + "message": "Bu riskləri və siyasət güncəlləmələrini qəbul edirəm" + }, "personalOwnership": { "message": "Fərdi sahiblik" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Bağlı" }, - "higherKDFIterations": { - "message": "Daha yüksək KDF iterasiyaları, ana parolunuzu təcavüzkar tərəfindən \"brute force\" hücumuna qarşı qorumağa kömək edir." - }, - "incrementsOf100,000": { - "message": "100,000-lik artım" - }, - "smallIncrements": { - "message": "kiçik artım" - }, "kdfIterationRecommends": { "message": "600,000 və ya daha çoxunu tövsiyə edirik" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "Daha köhnə cihazlar üçün KDF-inizi çox yüksək ayarlamaq performans problemlərinə səbəb ola bilər. $VALUE$ üzrə dəyəri artırın və cihazınızı test edin.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Abunəliyinizi bərpa etmək üçün Müştəri Dəstəyi ilə əlaqə saxlayın." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domen götürüldü" }, + "itemAddedToFavorites": { + "message": "Element sevimlilərə əlavə edildi" + }, + "itemRemovedFromFavorites": { + "message": "Element sevimlilərdən çıxarıldı" + }, + "copyNote": { + "message": "Notu kopyala" + }, "organizationNameMaxLength": { "message": "Təşkilat adı 50 xarakterdən çox ola bilməz." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "Biznes planlarına bax" + }, + "updateEncryptionSettings": { + "message": "Şifrələmə ayarlarını güncəlləyin" + }, + "updateYourEncryptionSettings": { + "message": "Şifrələmə ayarlarınızı güncəlləyin" + }, + "updateSettings": { + "message": "Güncəlləmə ayarları" + }, + "algorithm": { + "message": "Alqoritm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Bitwarden-in seyf verilərinizi necə şifrələyəcəyini seçin. Bütün seçimlər güvənlidir, ancaq daha güclü üsullar, xüsusən də brute-force hücumlarına qarşı daha yaxşı qoruma təklif edir. Bitwarden, əksər istifadəçilər üçün ilkin ayarı tövsiyə edir." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "İlkin dəyərdən yuxarı dəyərin təyin edilməsi, təhlükəsizliyi artırır, ancaq bu, seyfinizin kilidini açma prosesini uzada bilər." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "Şifrələmə alqoritmləri barədə" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 təhlükəsizlik və performansı tarazlaşdıran yaxşı test edilmiş şifrələmə üsuludur. Bütün istifadəçilər üçün idealdır." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id, müasir hücumlara qarşı daha güclü qoruma təklif edir. Güclü cihazlara sahib qabaqcıl istifadəçilər üçün idealdır." } } diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index da155bcd0a4..9bc30d40c03 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Апавешчаныя ўдзельнікі ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Налады ключа шыфравання" }, - "kdfAlgorithm": { - "message": "Алгарытм KDF" - }, "kdfIterations": { "message": "Ітэрацыя KDF" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Высокае значэнне ітэрацыі KDF, памяці і паралелізму дапаможа абараніць асноўны пароль ад атакі поўным пераборам." }, - "changeKdf": { - "message": "Змяніць KDF" - }, "encKeySettingsChanged": { "message": "Налады ключа шыфравання зменены" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Выдаліць асабістае сховішча" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Off" }, - "higherKDFIterations": { - "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." - }, - "incrementsOf100,000": { - "message": "increments of 100,000" - }, - "smallIncrements": { - "message": "small increments" - }, "kdfIterationRecommends": { "message": "We recommend 600,000 or more" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "For older devices, setting your KDF too high may lead to performance issues. Increase the value in $VALUE$ and test your devices.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index 8e9df78e164..dbb9103e7b3 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ нови пароли в риск", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Известени членове ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Настройки на шифриращия ключ" }, - "kdfAlgorithm": { - "message": "Алгоритъм KDF" - }, "kdfIterations": { "message": "Повторения за KDF" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "По-високите стойности за броя на повторения, паметта или степента на успоредно изпълнение на KDF може да защитят главната Ви парола от атаки тип „груба сила“." }, - "changeKdf": { - "message": "Смяна на KDF" - }, "encKeySettingsChanged": { "message": "Настройките за шифриращия ключ са сменени" }, @@ -5710,6 +5713,65 @@ "message": "Научете повече относно ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Налично сега" + }, + "autoConfirm": { + "message": "Автоматично потвърждение на новите потребители" + }, + "autoConfirmDescription": { + "message": "Новите потребители, поканени в организацията, ще бъдат потвърждавани автоматично, когато се отключи устройство на администратор.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "Как се включва автоматичното потвърждаване на потребителите" + }, + "autoConfirmStep1": { + "message": "Отворете добавката на Битуорден." + }, + "autoConfirmStep2a": { + "message": "Изберете", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Включване.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Добавката за браузър на Битуорден е отворена. Сега можете да включите настройката за автоматично потвърждаване на потребителите." + }, + "autoConfirmPolicyEditDescription": { + "message": "Новите потребители, поканени в организацията, ще бъдат потвърждавани автоматично, когато се отключи устройство на администратор. Преди да включите тази настройка, трябва да прегледате и да се съгласите със следното: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Потенциален риск за сигурността. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Автоматичното потвърждение на потребителите може да представлява риск за сигурността на данните на организацията." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Научете повече за рисковете", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Изисква се да е включена политиката за единствена организация. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Достъпът ще бъде преустановен за всеки, който е част от повече от една организация, докато не напусне другите организации." + }, + "autoConfirmSingleOrgExemption": { + "message": "Политиката за единствена организация ще се прилага за всички роли. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "Без авариен достъп. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Аварийният достъп ще бъде премахнат." + }, + "autoConfirmCheckBoxLabel": { + "message": "Приемам тези рискове и промени в политиката" + }, "personalOwnership": { "message": "Индивидуално притежание" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Изключено" }, - "higherKDFIterations": { - "message": "По-високите стойности за броя на повторения на KDF може да защитят главната Ви парола от атаки тип „груба сила“." - }, - "incrementsOf100,000": { - "message": "стъпки от 100 000" - }, - "smallIncrements": { - "message": "малки стъпки" - }, "kdfIterationRecommends": { "message": "Препоръчваме 600 000 или повече" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "Задаването на прекалено висока стойност за KDF може да натовари прекомерно по-старите устройства. Увеличете стойността на $VALUE$ и изпробвайте устройствата си.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Свържете се с поддръжката, за да възобновите абонамента си." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Домейнът е присвоен" }, + "itemAddedToFavorites": { + "message": "Елементът е добавен към любимите" + }, + "itemRemovedFromFavorites": { + "message": "Елементът е премахнат от любимите" + }, + "copyNote": { + "message": "Копиране на бележката" + }, "organizationNameMaxLength": { "message": "Името на организацията не може да бъде по-дълго от 50 знака." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "Преглед на плановете за компании" + }, + "updateEncryptionSettings": { + "message": "Обновяване на настройките за шифроване" + }, + "updateYourEncryptionSettings": { + "message": "Обновете настройките си за шифроване" + }, + "updateSettings": { + "message": "Обновяване на настройките" + }, + "algorithm": { + "message": "Алгоритъм" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Изберете как да шифрова Битуорден данните в трезора Ви. Всички варианти са сигурни, но по-сложните методи дават по-добра защита – особено срещу атаки от вида „груба сила“. За повечето потребители, Битуорден препоръчва стандартната настройка." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Увеличаването на стойностите над стандартните ще подобри сигурността, но може да направи отключването на трезора по-бавно." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "Информация относно алгоритмите за шифроване" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 е изпитан метод за шифроване, който дава баланс между сигурност и бързина. Подходящ за всички потребители." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id предлага по-добра защита срещу съвременните атаки. Подходящ за напреднали потребители с по-мощни устройства." } } diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index dc900e83525..19ba18f741f 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Encryption key settings" }, - "kdfAlgorithm": { - "message": "KDF algorithm" - }, "kdfIterations": { "message": "KDF iterations" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Higher KDF iterations, memory, and parallelism can help protect your master password from being brute forced by an attacker." }, - "changeKdf": { - "message": "Change KDF" - }, "encKeySettingsChanged": { "message": "Encryption key settings saved" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Off" }, - "higherKDFIterations": { - "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." - }, - "incrementsOf100,000": { - "message": "increments of 100,000" - }, - "smallIncrements": { - "message": "small increments" - }, "kdfIterationRecommends": { "message": "We recommend 600,000 or more" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "For older devices, setting your KDF too high may lead to performance issues. Increase the value in $VALUE$ and test your devices.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index 7040e9aa3d5..eeed875c587 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Encryption key settings" }, - "kdfAlgorithm": { - "message": "KDF algorithm" - }, "kdfIterations": { "message": "KDF iterations" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Higher KDF iterations, memory, and parallelism can help protect your master password from being brute forced by an attacker." }, - "changeKdf": { - "message": "Change KDF" - }, "encKeySettingsChanged": { "message": "Encryption key settings saved" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Off" }, - "higherKDFIterations": { - "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." - }, - "incrementsOf100,000": { - "message": "increments of 100,000" - }, - "smallIncrements": { - "message": "small increments" - }, "kdfIterationRecommends": { "message": "We recommend 600,000 or more" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "For older devices, setting your KDF too high may lead to performance issues. Increase the value in $VALUE$ and test your devices.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index 142422a6e95..977b173ff0c 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Membres notificats ($COUNT$)", "placeholders": { @@ -2024,7 +2033,7 @@ "message": "Canvia el correu electrònic" }, "changeEmailTwoFactorWarning": { - "message": "Si continueu, canviarà l'adreça de correu electrònic del vostre compte. No canviarà l'adreça de correu electrònic utilitzada per a l'autenticació de dos factors. Podeu canviar aquesta adreça de correu electrònic a la configuració d'inici de sessió en dues passes." + "message": "Si continueu, canviarà l'adreça de correu electrònic del vostre compte. No canviarà l'adreça de correu electrònic utilitzada per a l'autenticació de doble factor. Podeu canviar aquesta adreça de correu electrònic a la configuració d'inici de sessió en dos passos." }, "newEmail": { "message": "Nou correu electrònic" @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Configuració de claus de xifratge" }, - "kdfAlgorithm": { - "message": "Algorisme de KDF" - }, "kdfIterations": { "message": "Iteracions de KDF" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Les iteracions de KDF, la memòria i el paral·lelisme més alts poden ajudar a protegir la vostra contrasenya mestra de ser forçada per força bruta per un atacant." }, - "changeKdf": { - "message": "Canvia KDF" - }, "encKeySettingsChanged": { "message": "S'ha canviat la configuració de les claus de xifratge" }, @@ -2130,10 +2133,10 @@ "message": "Desautoritza sessions" }, "deauthorizeSessionsDesc": { - "message": "Voleu evitar que el vostre compte s'inicie en un altre dispositiu? Seguiu aquestes passes per desautoritzar tots els ordinadors o dispositius que hàgeu utilitzat prèviament. Aquest pas de seguretat es recomana si anteriorment heu utilitzat un ordinador públic o si heu guardat la contrasenya accidentalment en un dispositiu que no és vostre. Aquest pas també esborrarà totes les sessions d'inici de sessió en dues passes recordades prèviament." + "message": "Voleu evitar que el vostre compte s'inicie en un altre dispositiu? A continuació podeu desautoritzar tots els ordinadors o dispositius que hàgeu utilitzat prèviament. Aquest pas de seguretat es recomana si anteriorment heu utilitzat un ordinador públic o si heu guardat la contrasenya accidentalment en un dispositiu que no és vostre. Aquest pas també esborrarà totes les sessions d'inici de sessió en dos passos recordades prèviament." }, "deauthorizeSessionsWarning": { - "message": "El procediment també tancarà la sessió actual, i l'heu de tornar a iniciar. També demanarà iniciar la sessió en dues passes, si està habilitada. Les sessions actives d'altres dispositius poden mantenir-se actives fins a una hora." + "message": "El procediment també tancarà la sessió actual, i l'heu de tornar a iniciar. També demanarà iniciar la sessió en dos passos, si està habilitada. Les sessions actives d'altres dispositius poden mantenir-se actives fins a una hora." }, "newDeviceLoginProtection": { "message": "New device login" @@ -2385,7 +2388,7 @@ "message": "Dominis guardats" }, "twoStepLogin": { - "message": "Inici de sessió en dues passes" + "message": "Inici de sessió en dos passos" }, "twoStepLoginEnforcement": { "message": "Aplicació d'inici de sessió en dos passos" @@ -2410,7 +2413,7 @@ "message": "Si heu configurat l'SSO o teniu previst fer-ho, és possible que l'inici de sessió en dos passos ja s'aplique a través del vostre proveïdor d'identitat." }, "twoStepLoginRecoveryWarning": { - "message": "Si habiliteu l'inici de sessió en dues passes, pot bloquejar-vos de manera definitiva el compte de Bitwarden. Un codi de recuperació us permet accedir al vostre compte en cas que no pugueu utilitzar el proveïdor d'inici de sessió en dues passes (p. Ex. Perdre el dispositiu). El suport de Bitwarden no podrà ajudar-vos si perdeu l'accés al vostre compte. Us recomanem que escriviu o imprimiu el codi de recuperació i el mantingueu en un lloc segur." + "message": "Si habiliteu l'inici de sessió en dos passos, pot bloquejar-vos de manera definitiva el compte de Bitwarden. Un codi de recuperació us permet accedir al vostre compte en cas que no pugueu utilitzar el proveïdor d'inici de sessió en dos passos (p. Ex. Perdre el dispositiu). El suport de Bitwarden no podrà ajudar-vos si perdeu l'accés al vostre compte. Us recomanem que escriviu o imprimiu el codi de recuperació i el mantingueu en un lloc segur." }, "restrictedItemTypePolicy": { "message": "Remove card item type" @@ -2496,7 +2499,7 @@ "message": "Aquest proveïdor d'inici de sessió en dos passos està habilitat al vostre compte." }, "twoStepLoginAuthDesc": { - "message": "Introduïu la vostra contrasenya mestra per modificar la configuració d'inici de sessió en dues passes." + "message": "Introduïu la vostra contrasenya mestra per modificar la configuració d'inici de sessió en dos passos." }, "twoStepAuthenticatorInstructionPrefix": { "message": "Download an authenticator app such as" @@ -2628,7 +2631,7 @@ "message": "Nom de l'amfitrió d'API" }, "twoFactorEmailDesc": { - "message": "Seguiu aquestes passes per configurar l'inici de sessió en dues passes amb el correu electrònic:" + "message": "Seguiu aquestes passes per configurar l'inici de sessió en dos passos amb el correu electrònic:" }, "twoFactorEmailEnterEmail": { "message": "Introduïu el correu electrònic amb el que voleu rebre els codis de verificació" @@ -2682,7 +2685,7 @@ "message": "Hi ha hagut un problema en llegir la clau de seguretat. Torneu-ho a provar." }, "twoFactorRecoveryYourCode": { - "message": "El codi de recuperació d'inici de sessió en dues passes de Bitwarden" + "message": "El codi de recuperació d'inici de sessió en dos passos de Bitwarden" }, "twoFactorRecoveryNoCode": { "message": "Encara no heu habilitat cap proveïdor d'inici de sessió en dos passos. Després d'activar-lo, podeu consultar ací el codi de recuperació." @@ -3667,7 +3670,7 @@ "message": "You have 0 invites remaining." }, "userUsingTwoStep": { - "message": "Aquest usuari fa servir l'inici de sessió en dues passes per protegir el seu compte." + "message": "Aquest usuari fa servir l'inici de sessió en dos passos per protegir el seu compte." }, "search": { "message": "Cerca" @@ -3772,7 +3775,7 @@ "message": "S'ha produït un error d'inici de sessió amb una contrasenya incorrecta." }, "failedLogin2fa": { - "message": "S'ha produït un error amb un inici de sessió en dues passes incorrecte." + "message": "S'ha produït un error amb un inici de sessió en dos passos incorrecte." }, "incorrectPassword": { "message": "Contrasenya incorrecta" @@ -4468,13 +4471,13 @@ "message": "Recorda el correu electronic" }, "recoverAccountTwoStepDesc": { - "message": "Si no podeu accedir al vostre compte a través dels vostres mètodes d'inici de sessió de dues passes, podeu utilitzar el codi de recuperació de l'inici de sessió en dues passes per desactivar tots els proveïdors del vostre compte." + "message": "Si no podeu accedir al vostre compte a través dels vostres mètodes d'inici de sessió en dos passos, podeu utilitzar el codi de recuperació de l'inici de sessió en dos passos per desactivar tots els proveïdors del vostre compte." }, "logInBelowUsingYourSingleUseRecoveryCode": { "message": "Log in below using your single-use recovery code. This will turn off all two-step providers on your account." }, "recoverAccountTwoStep": { - "message": "Recupera l'inici de sessió en dues passes del compte" + "message": "Recupera l'inici de sessió en dos passos del compte" }, "twoStepRecoverDisabled": { "message": "S'ha inhabilitat l'inici de sessió en dos passos al vostre compte." @@ -5094,7 +5097,7 @@ "message": "Cal iniciar sessió en dos passos" }, "twoStepLoginPolicyDesc": { - "message": "Requereix que els usuaris configuren l’inici de sessió en dues passes als seus comptes personals." + "message": "Requereix que els usuaris configuren l’inici de sessió en dos passos als seus comptes personals." }, "twoStepLoginPolicyWarning": { "message": "Els membres de l’organització que no tinguen activat l’inici de sessió en dos passos per al seu compte personal s'eliminaran de l’organització i rebran un correu electrònic que els notificarà sobre el canvi." @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Suprimeix la caixa forta individual" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Off" }, - "higherKDFIterations": { - "message": "Les iteracions mes altes de KDF poden ajudar a protegir la contrasenya mestra de ser forçada per un atacant." - }, - "incrementsOf100,000": { - "message": "increments of 100,000" - }, - "smallIncrements": { - "message": "small increments" - }, "kdfIterationRecommends": { "message": "Recomanem 600.000 o més" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "Per a dispositius més antics, configurar el KDF a un valor massa alt pot provocar problemes de rendiment. Augmenteu el valor de $VALUE$ i proveu els dispositius.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index 796db063b21..5a9e6c1a6d2 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ nových ohrožených hesel", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Obeznámení členové ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Nastavení šifrovacího klíče" }, - "kdfAlgorithm": { - "message": "Algoritmus KDF" - }, "kdfIterations": { "message": "Iterace KDF" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Vyšší hodnota iterací KDF, paměti a paralelity pomáhá chránit Vaše hlavní heslo před útokem hrubou silou (brute force attack)." }, - "changeKdf": { - "message": "Změnit KDF" - }, "encKeySettingsChanged": { "message": "Nastavení šifrovacího klíče bylo uloženo" }, @@ -5710,6 +5713,65 @@ "message": "Více informací o ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Nyní k dispozici" + }, + "autoConfirm": { + "message": "Automatické potvrzení nových uživatelů" + }, + "autoConfirmDescription": { + "message": "Noví uživatelé pozvaní do organizace budou automaticky potvrzeni při odemknutí zařízení správce.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "Jak zapnout automatické potvrzení uživatele" + }, + "autoConfirmStep1": { + "message": "Otevřete rozšíření Bitwarden." + }, + "autoConfirmStep2a": { + "message": "Vyberte", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Zapnout.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Rozšíření prohlížeče Bitwarden bylo úspěšně otevřeno. Nyní můžete aktivovat automatické potvrzení uživatele." + }, + "autoConfirmPolicyEditDescription": { + "message": "Noví uživatelé pozvaní do organizace budou automaticky potvrzeni při odemknutí zařízení správce. Před zapnutím této zásady zkontrolujte a odsouhlaste následující: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potenciální bezpečnostní riziko. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatické potvrzení uživatele by mohlo představovat bezpečnostní riziko pro data Vaší organizace." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Více o rizicích", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Vyžadují se jednotné zásady organizace. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Kdokoli, kdo je členem více než jedné organizace, bude mít přístup odvolán do té doby, než opustí ostatní organizace." + }, + "autoConfirmSingleOrgExemption": { + "message": "Jednotné zásady organizace se budou vztahovat na všechny role. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "Žádný nouzový přístup. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Nouzový přístup bude odebrán." + }, + "autoConfirmCheckBoxLabel": { + "message": "Přijímám tato rizika a aktualizace zásad" + }, "personalOwnership": { "message": "Odebrat osobní trezor" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Vypnuto" }, - "higherKDFIterations": { - "message": "Více iterací KDF Vám může pomoci ochránit hlavní heslo před útočníkem, který by ho vylákal hrubou silou." - }, - "incrementsOf100,000": { - "message": "přírůstky po 100 000" - }, - "smallIncrements": { - "message": "malé přírůstky" - }, "kdfIterationRecommends": { "message": "Doporučujeme 600 000 nebo více" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "U starších zařízení může příliš vysoké nastavení KDF vést k problémům s výkonem. Zvyšte hodnotu v $VALUE$ a otestujte svá zařízení.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Pro obnovení předplatného se obraťte na zákaznickou podporu." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Doména uplatněna" }, + "itemAddedToFavorites": { + "message": "Položka byla přidána do oblíbených" + }, + "itemRemovedFromFavorites": { + "message": "Položka byla odebrána z oblíbených" + }, + "copyNote": { + "message": "Kopírovat poznámku" + }, "organizationNameMaxLength": { "message": "Název organizace nesmí přesáhnout 50 znaků." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "Zobrazit obchodní plány" + }, + "updateEncryptionSettings": { + "message": "Aktualizovat nastavení šifrování" + }, + "updateYourEncryptionSettings": { + "message": "Aktualizujte nastavení šifrování" + }, + "updateSettings": { + "message": "Aktualizovat nastavení" + }, + "algorithm": { + "message": "Algoritmus" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Vyberte, jak by Bitwarden měl šifrovat Vaše data trezoru. Všechny volby jsou bezpečné, ale silnější metody poskytují lepší ochranu - zejména proti útokům silou. Bitwarden doporučuje výchozí nastavení pro většinu uživatelů." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Zvýšení hodnot nad výchozím nastavením zvýší zabezpečení, ale odemknutí Vašeho trezoru může trvat déle." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "O šifrovacích algoritmech" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 je dobře testovaná šifrovací metoda, která vyvažuje bezpečnost a výkon. Dobré pro všechny uživatele." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id nabízí silnější ochranu proti moderním útokům. Nejlepší pro pokročilé uživatele s výkonnými zařízeními." } } diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index 7361ac14f76..794f717d9b8 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Encryption key settings" }, - "kdfAlgorithm": { - "message": "KDF algorithm" - }, "kdfIterations": { "message": "KDF iterations" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Higher KDF iterations, memory, and parallelism can help protect your master password from being brute forced by an attacker." }, - "changeKdf": { - "message": "Change KDF" - }, "encKeySettingsChanged": { "message": "Encryption key settings saved" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Off" }, - "higherKDFIterations": { - "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." - }, - "incrementsOf100,000": { - "message": "increments of 100,000" - }, - "smallIncrements": { - "message": "small increments" - }, "kdfIterationRecommends": { "message": "We recommend 600,000 or more" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "For older devices, setting your KDF too high may lead to performance issues. Increase the value in $VALUE$ and test your devices.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index b5103d4d6ba..5f3c0658b03 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Underrettede medlemmer ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Indstillinger for krypteringsnøgle" }, - "kdfAlgorithm": { - "message": "KDF-algoritme" - }, "kdfIterations": { "message": "KDF-iterationer" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Øget KDF-iterationer, hukommelse og parallelitet kan hjælpe med at beskytte hovedadgangskoden mod brute force-angreb." }, - "changeKdf": { - "message": "Ændr KDF" - }, "encKeySettingsChanged": { "message": "Indstillinger for krypteringsnøgle gemt" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Fjern individuel boks" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Fra" }, - "higherKDFIterations": { - "message": "Højere KDF-iterationer kan hjælpe med at beskytte hovedadgangskoden mod brute force-angreb." - }, - "incrementsOf100,000": { - "message": "forøgelsesintervaller på 100.000" - }, - "smallIncrements": { - "message": "små forøgelser" - }, "kdfIterationRecommends": { "message": "Vi anbefaler 600.000 eller højere" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "Indstilling af for høj KDF kan på ældre enheder medføre ydeevneproblemer. Forøg værdien i $VALUE$ og test på enhederne.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Kontakt kundesupport for at genoprette abonnementet." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domæne registreret" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organisationsnavn må ikke overstige 50 tegn." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index b422165fc9e..515581c7769 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ neue gefährdete Passwörter", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Benachrichtigte Mitglieder ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Verschlüsselungsschlüssel-Einstellungen" }, - "kdfAlgorithm": { - "message": "KDF-Algorithmus" - }, "kdfIterations": { "message": "KDF-Iterationen" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Höhere KDF-Iterationen, -Speicher und -Parallelität helfen dabei, dein Master-Passwort besser vor Brute-Force-Angriffen zu schützen." }, - "changeKdf": { - "message": "KDF ändern" - }, "encKeySettingsChanged": { "message": "Verschlüsselungsschlüssel-Einstellungen wurden geändert" }, @@ -5710,6 +5713,65 @@ "message": "Erfahre mehr über den ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Persönlichen Tresor entfernen" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Aus" }, - "higherKDFIterations": { - "message": "Höhere KDF-Iterationen können helfen, dein Master-Passwort vor Brute-Force-Attacken durch einen Angreifer zu schützen." - }, - "incrementsOf100,000": { - "message": "Schritten von 100.000" - }, - "smallIncrements": { - "message": "kleinen Schritten" - }, "kdfIterationRecommends": { "message": "Wir empfehlen 600.000 oder mehr" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "Auf älteren Geräten kann es durch zu hohe KDF-Iterationen zu Leistungsproblemen kommen. Erhöhe den Wert in $VALUE$ und teste es auf deinen Geräten.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Kontaktiere den Kundensupport, um dein Abonnement wiederherzustellen." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain beansprucht" }, + "itemAddedToFavorites": { + "message": "Eintrag zu Favoriten hinzugefügt" + }, + "itemRemovedFromFavorites": { + "message": "Eintrag aus Favoriten entfernt" + }, + "copyNote": { + "message": "Notiz kopieren" + }, "organizationNameMaxLength": { "message": "Der Name der Organisation darf 50 Zeichen nicht überschreiten." }, @@ -11295,10 +11348,10 @@ "description": "Verb" }, "unArchive": { - "message": "Archivieren rück­gän­gig ma­chen" + "message": "Nicht mehr archivieren" }, "itemsInArchive": { - "message": "Archiveinträge" + "message": "Einträge im Archiv" }, "noItemsInArchive": { "message": "Keine Einträge im Archiv" @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "Unternehmens-Tarife anzeigen" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index fb022954df0..8bd0ecaee1b 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Ειδοποιημένα μέλη ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Ρυθμίσεις Κλειδιού Κρυπτογράφησης" }, - "kdfAlgorithm": { - "message": "Αλγόριθμος KDF" - }, "kdfIterations": { "message": "Επαναλήψεις KDF" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Οι περισσότερες επαναλήψεις KDF, η μνήμη και ο παραλληλισμός μπορούν να συμβάλουν στην προστασία του κύριου κωδικού πρόσβασής σας από επιθέσεις τύπου «brute force»." }, - "changeKdf": { - "message": "Αλλαγή KDF" - }, "encKeySettingsChanged": { "message": "Οι Ρυθμίσεις του Κλειδιού Κρυπτογράφησης Αλλαξαν" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Προσωπική Ιδιοκτησία" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Ανενεργή" }, - "higherKDFIterations": { - "message": "Οι περισσότερες επαναλήψεις KDF μπορούν να συμβάλουν στην προστασία του κύριου κωδικού πρόσβασής σας από επιθέσεις τύπου «brute force»." - }, - "incrementsOf100,000": { - "message": "προσαυξήσεις των 100.000" - }, - "smallIncrements": { - "message": "μικρές προσαυξήσεις" - }, "kdfIterationRecommends": { "message": "Προτείνουμε 600.000 ή περισσότερα" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "Για τις παλαιότερες συσκευές, η επιλογή πάρα πολλών επαναλήψεων KDF ενδέχεται να προκαλέσει ζητήματα επιδόσεων. Αυξήστε την τιμή σε $VALUE$ και δοκιμάστε τις συσκευές σας.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index 0ed7f46e4a5..09d507efd08 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Encryption key settings" }, - "kdfAlgorithm": { - "message": "KDF algorithm" - }, "kdfIterations": { "message": "KDF iterations" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Higher KDF iterations, memory, and parallelism can help protect your master password from being brute forced by an attacker." }, - "changeKdf": { - "message": "Change KDF" - }, "encKeySettingsChanged": { "message": "Encryption key settings changed" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organisation will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organisation will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organisation’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organisation policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organisation will have their access revoked until they leave the other organisations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organisation policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Off" }, - "higherKDFIterations": { - "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." - }, - "incrementsOf100,000": { - "message": "increments of 100,000" - }, - "smallIncrements": { - "message": "small increments" - }, "kdfIterationRecommends": { "message": "We recommend 600,000 or more" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "For older devices, setting your KDF too high may lead to performance issues. Increase the value in $VALUE$ and test your devices.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favourites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favourites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organisation name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index cc2c1561e20..a265546c82c 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Encryption key settings" }, - "kdfAlgorithm": { - "message": "KDF algorithm" - }, "kdfIterations": { "message": "KDF iterations" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Higher KDF iterations, memory, and parallelism can help protect your master password from being brute forced by an attacker." }, - "changeKdf": { - "message": "Change KDF" - }, "encKeySettingsChanged": { "message": "Encryption key settings changed" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organisation will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organisation will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organisation’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organisation policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organisation will have their access revoked until they leave the other organisations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organisation policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Personal Ownership" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Off" }, - "higherKDFIterations": { - "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." - }, - "incrementsOf100,000": { - "message": "increments of 100,000" - }, - "smallIncrements": { - "message": "small increments" - }, "kdfIterationRecommends": { "message": "We recommend 600,000 or more" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "For older devices, setting your KDF too high may lead to performance issues. Increase the value in $VALUE$ and test your devices.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favourites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favourites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organisation name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index d96cef99644..88add866744 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Sciigitaj membroj ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Agordoj pri ĉifra ŝlosilo" }, - "kdfAlgorithm": { - "message": "KDF-Algoritmo" - }, "kdfIterations": { "message": "KDF-Ripetoj" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Higher KDF iterations, memory, and parallelism can help protect your master password from being brute forced by an attacker." }, - "changeKdf": { - "message": "Ŝanĝi KDF" - }, "encKeySettingsChanged": { "message": "Ŝanĝaj Ŝlosilaj Agordoj Ŝanĝiĝis" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Persona Posedo" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Malŝaltita" }, - "higherKDFIterations": { - "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." - }, - "incrementsOf100,000": { - "message": "increments of 100,000" - }, - "smallIncrements": { - "message": "small increments" - }, "kdfIterationRecommends": { "message": "We recommend 600,000 or more" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "For older devices, setting your KDF too high may lead to performance issues. Increase the value in $VALUE$ and test your devices.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index 074513343e3..db3dd22cc5c 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Miembros notificados ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Configuración de clave de cifrado" }, - "kdfAlgorithm": { - "message": "Algoritmo KDF" - }, "kdfIterations": { "message": "Iteraciones de KDF" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Las iteraciones, memorias y paralelismos de KDF más altos pueden ayudar a proteger su contraseña maestra de ser calculada por un atacante." }, - "changeKdf": { - "message": "Modificar KDF" - }, "encKeySettingsChanged": { "message": "Se cambió la configuración de clave de cifrado" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Propiedad personal" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Off" }, - "higherKDFIterations": { - "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." - }, - "incrementsOf100,000": { - "message": "incrementos de 100.000" - }, - "smallIncrements": { - "message": "pequeños incrementos" - }, "kdfIterationRecommends": { "message": "Recomendamos 600.000 o más" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "For older devices, setting your KDF too high may lead to performance issues. Increase the value in $VALUE$ and test your devices.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index 78323078a92..306dc5aaab7 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Teavitatud liikmed ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Krüpteerimise võtme seaded" }, - "kdfAlgorithm": { - "message": "KDF algoritm" - }, "kdfIterations": { "message": "KDF iteratsioonid" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Rohkem KDF kordusi, mälumahtu ja paralleelsust aitab sul kaitsta ülemparooli toore jõuga lahti murdmisest ründaja poolt." }, - "changeKdf": { - "message": "Muuda KDF-i" - }, "encKeySettingsChanged": { "message": "Krüpteerimise võtme seaded on muudetud" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Personaalne salvestamine" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Off" }, - "higherKDFIterations": { - "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." - }, - "incrementsOf100,000": { - "message": "increments of 100,000" - }, - "smallIncrements": { - "message": "small increments" - }, "kdfIterationRecommends": { "message": "We recommend 600,000 or more" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "For older devices, setting your KDF too high may lead to performance issues. Increase the value in $VALUE$ and test your devices.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index 3eaa56371e4..1339a2b8cec 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Zifratze-gakoaren ezarpenak" }, - "kdfAlgorithm": { - "message": "KDF algoritmoa" - }, "kdfIterations": { "message": "KDF iterazioak" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Higher KDF iterations, memory, and parallelism can help protect your master password from being brute forced by an attacker." }, - "changeKdf": { - "message": "Aldatu KDF" - }, "encKeySettingsChanged": { "message": "Zifratze-gakoaren ezarpenak aldatuta" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Ezabatu kutxa gotor pertsonala" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Off" }, - "higherKDFIterations": { - "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." - }, - "incrementsOf100,000": { - "message": "increments of 100,000" - }, - "smallIncrements": { - "message": "small increments" - }, "kdfIterationRecommends": { "message": "We recommend 600,000 or more" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "For older devices, setting your KDF too high may lead to performance issues. Increase the value in $VALUE$ and test your devices.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index 7928cae4c77..121dfa49fca 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "اعضا مطلع شدند ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "تنظیمات کلید رمزگذاری" }, - "kdfAlgorithm": { - "message": "الگوریتم KDF" - }, "kdfIterations": { "message": "تکرار KDF" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "تکرارهای KDF بالاتر، حافظه و موازی سازی می‌تواند به محافظت از کلمه عبور اصلی شما در برابر حمله بروت فورث توسط مهاجم کمک کند." }, - "changeKdf": { - "message": "تغییر KDF" - }, "encKeySettingsChanged": { "message": "تنظیمات کلید رمزگذاری ذخیره شد" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "حذف گاوصندوق شخصی" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "خاموش" }, - "higherKDFIterations": { - "message": "افزایش تعداد تکرارهای KDF می‌تواند به محافظت از کلمه عبور اصلی شما در برابر حملات بروت‌فورس کمک کند." - }, - "incrementsOf100,000": { - "message": "افزایش‌ها به صورت ۱۰۰,۰۰۰ تایی" - }, - "smallIncrements": { - "message": "افزایش‌های کوچک" - }, "kdfIterationRecommends": { "message": "ما توصیه می‌کنیم ۶۰۰,۰۰۰ یا بیشتر باشد" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "برای دستگاه‌های قدیمی‌تر، تنظیم KDF خیلی بالا ممکن است باعث مشکلات عملکردی شود. مقدار را در $VALUE$ افزایش دهید و دستگاه‌های خود را تست کنید.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " برای بازیابی اشتراک خود با پشتیبانی مشتری تماس بگیرید." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "دامنه ثبت شده است" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "نام سازمان نمی‌تواند بیش از ۵۰ کاراکتر باشد." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index 68cfc419f44..ffa7582b0a0 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Ilmoitetut jäsenet ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Salausavainten asetukset" }, - "kdfAlgorithm": { - "message": "KDF-algoritmi" - }, "kdfIterations": { "message": "KDF-toistot" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Korkeammat KDF-toistojen, -muistin ja -rinnakkaisuuksien määrät vahvistavat pääsalasanasi suojausta väsytyshyökkäyksien varalta." }, - "changeKdf": { - "message": "Vaihda KDF-asetuksia" - }, "encKeySettingsChanged": { "message": "Salausavainten asetukset tallennettiin" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Poista yksityinen holvi" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Ei käytössä" }, - "higherKDFIterations": { - "message": "Korkeampi KDF-toistojen määrä vahvistaa pääsalasanasi suojausta väsytyshyökkäyksien varalta." - }, - "incrementsOf100,000": { - "message": "100 000 välein" - }, - "smallIncrements": { - "message": "vähitellen" - }, "kdfIterationRecommends": { "message": "Vähimmäissuositus on 600 000" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "Vanhemmilla laitteilla liian korkea KDF-arvo voi aiheuttaa suorituskykyongelmiin. Lisää arvoa $VALUE$ ja kokeile muutoksia laitteillasi.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Aktivoi tilauksesi uudelleen olemalla yhteydessä asiakaspalveluun." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index c374ea6e751..6342f27966c 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Mga setting ng encryption key" }, - "kdfAlgorithm": { - "message": "Algoritmo ng KDF" - }, "kdfIterations": { "message": "Iterasyon ng KDF" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Makakatulong maprotektahan ng mataas na iterasyon ng KDF, memorya, at pararelismo ang master password mo na ma-brute force ng masasamang-loob." }, - "changeKdf": { - "message": "Baguhin ang KDF" - }, "encKeySettingsChanged": { "message": "Na-save ang mga setting ng encryption key" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Alisin ang indibidwal na vault" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Off" }, - "higherKDFIterations": { - "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." - }, - "incrementsOf100,000": { - "message": "increments of 100,000" - }, - "smallIncrements": { - "message": "small increments" - }, "kdfIterationRecommends": { "message": "We recommend 600,000 or more" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "For older devices, setting your KDF too high may lead to performance issues. Increase the value in $VALUE$ and test your devices.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index 5c8b24a8b4d..ff9f26dd353 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ mots de passe à risque", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Membres notifiés ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Paramètres de la clé de chiffrement" }, - "kdfAlgorithm": { - "message": "Algorithme KDF" - }, "kdfIterations": { "message": "Itérations KDF" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Des itérations KDF, une mémoire et un parallélisme plus élevés peuvent contribuer à protéger votre mot de passe principal contre l'attaque par force brute d'un assaillant." }, - "changeKdf": { - "message": "Changer KDF" - }, "encKeySettingsChanged": { "message": "Paramètres de la clé de chiffrement modifiés" }, @@ -5710,6 +5713,65 @@ "message": "Apprenez-en plus sur ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Supprimer le coffre individuel" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Désactivé" }, - "higherKDFIterations": { - "message": "Des itérations KDF plus élevées peuvent aider à protéger votre mot de passe principal contre la force brute d'un assaillant." - }, - "incrementsOf100,000": { - "message": "incréments de 100 000" - }, - "smallIncrements": { - "message": "petits incréments" - }, "kdfIterationRecommends": { "message": "Nous vous recommandons 600 000 ou plus" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "Pour les appareils plus anciens, régler le nombre d'itérations KDF trop haut peut entraîner des problèmes de performance. Augmentez la valeur dans $VALUE$ et testez vos appareils.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contactez le Support Client pour rétablir votre abonnement." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domaine réclamé" }, + "itemAddedToFavorites": { + "message": "Élément ajouté aux favoris" + }, + "itemRemovedFromFavorites": { + "message": "Élément retiré des favoris" + }, + "copyNote": { + "message": "Copier la note" + }, "organizationNameMaxLength": { "message": "Le nom de l'organisation ne doit pas dépasser 50 caractères." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "Voir les forfaits d'affaires" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index 2079116a304..18d9cd9cd65 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Encryption key settings" }, - "kdfAlgorithm": { - "message": "KDF algorithm" - }, "kdfIterations": { "message": "KDF iterations" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Higher KDF iterations, memory, and parallelism can help protect your master password from being brute forced by an attacker." }, - "changeKdf": { - "message": "Change KDF" - }, "encKeySettingsChanged": { "message": "Encryption key settings saved" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Off" }, - "higherKDFIterations": { - "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." - }, - "incrementsOf100,000": { - "message": "increments of 100,000" - }, - "smallIncrements": { - "message": "small increments" - }, "kdfIterationRecommends": { "message": "We recommend 600,000 or more" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "For older devices, setting your KDF too high may lead to performance issues. Increase the value in $VALUE$ and test your devices.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index 66fd9ef8afc..dbc95083710 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "חברים שהודיעו להם ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "הגדרות מפתח הצפנה" }, - "kdfAlgorithm": { - "message": "אלגוריתם KDF" - }, "kdfIterations": { "message": "חזרות KDF" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "ערכי חזרות, זיכרון, ומקבילות KDF גבוהים יותר יכולים לעזור להגן על הסיסמה הראשית מפני תקיפה כוחנית על ידי תוקף." }, - "changeKdf": { - "message": "שנה KDF" - }, "encKeySettingsChanged": { "message": "הגדרות מפתח ההצפנה נשמרו" }, @@ -5710,6 +5713,65 @@ "message": "למד עוד על ה", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "הסר כספת אישית" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "כבוי" }, - "higherKDFIterations": { - "message": "ערכי חזרות KDF גבוהים יותר יכולים לעזור להגן על הסיסמה הראשית מפני תקיפה כוחנית על ידי תוקף." - }, - "incrementsOf100,000": { - "message": "במרווחים של 100,000" - }, - "smallIncrements": { - "message": "במרווחים קטנים" - }, "kdfIterationRecommends": { "message": "אנו ממליצים על 600,000 או יותר" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "עבור מכשירים ישנים יותר, הגדרת ה־KDF שלך לערך גבוה מדי עשויה להוביל לבעיות ביצועים. הגדל את הערך ב־$VALUE$ ובדוק את המכשירים שלך.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " צור קשר עם שירות הלקוחות כדי להחזיר את המנוי שלך." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "דומיין נדרש" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "שם ארגון לא יכול לחרוג מ־50 תווים." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index f6e6e9f9a30..42b7a424840 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Encryption key settings" }, - "kdfAlgorithm": { - "message": "KDF algorithm" - }, "kdfIterations": { "message": "KDF iterations" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Higher KDF iterations, memory, and parallelism can help protect your master password from being brute forced by an attacker." }, - "changeKdf": { - "message": "Change KDF" - }, "encKeySettingsChanged": { "message": "Encryption key settings saved" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Off" }, - "higherKDFIterations": { - "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." - }, - "incrementsOf100,000": { - "message": "increments of 100,000" - }, - "smallIncrements": { - "message": "small increments" - }, "kdfIterationRecommends": { "message": "We recommend 600,000 or more" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "For older devices, setting your KDF too high may lead to performance issues. Increase the value in $VALUE$ and test your devices.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index 91e7db0af7c..f057a77ab5b 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "Novih rizičnih lozinki: $COUNT$", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Obaviješteni članovi ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Postavke ključa za šifriranje" }, - "kdfAlgorithm": { - "message": "KDF algoritam" - }, "kdfIterations": { "message": "KDF iteracija" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Više KDF iteracije, memiorije i paralelizmi mogu pomoći zaštititi tvoju glavnu lozinku od bute force napada." }, - "changeKdf": { - "message": "Promijeni KDF" - }, "encKeySettingsChanged": { "message": "Postavke ključa za šifriranje promijenjene" }, @@ -5710,6 +5713,65 @@ "message": "Saznaj više o ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Dostupno sada" + }, + "autoConfirm": { + "message": "Automatska potvrda novih korisnika" + }, + "autoConfirmDescription": { + "message": "Novi korisnici pozvani u organizaciju bit će automatski potvrđeni kada se uređaj administratora otključa.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "Kako uključiti automatsku potvrdu korisnika" + }, + "autoConfirmStep1": { + "message": "Otvori svoje Bitwarden proširenje." + }, + "autoConfirmStep2a": { + "message": "Odaberi", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Uključi.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Uspješno otvoreno Bitwarden proširenje za preglednik. Sada možeš aktivirati postavku automatske potvrde korisnika." + }, + "autoConfirmPolicyEditDescription": { + "message": "Novi korisnici pozvani u organizaciju bit će automatski potvrđeni kada se uređaj administratora otključa. Prije uključivanja ove politike, pregledaj i prihvati sljedeće: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Mogući sigurnosni rizik. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatska potvrda korisnika mogla bi predstavljati sigurnosni rizik za podatke tvoje organizacije." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Više o rizicima", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Potrebno je pravilo Isključive organizacije. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Svakome tko je dio više od jedne organizacije bit će ukinut pristup sve dok ne napusti ostale organizacije." + }, + "autoConfirmSingleOrgExemption": { + "message": "Pravilo Isključive organizacija će biti primijenjeno na sve uloge." + }, + "autoConfirmNoEmergencyAccess": { + "message": "Nema pristupa u nuždi. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Pristup u nuždi biti će uklonjen." + }, + "autoConfirmCheckBoxLabel": { + "message": "Prihavaćam ove rizike i ažurirana pravila" + }, "personalOwnership": { "message": "Ukloni osobni trezor" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Isklj." }, - "higherKDFIterations": { - "message": "Veće KDF iteracije mogu pomoći u zaštiti tvoje glavne lozinke od napadača." - }, - "incrementsOf100,000": { - "message": "koracima od 100.000" - }, - "smallIncrements": { - "message": "malim koracima" - }, "kdfIterationRecommends": { "message": "Preporučujemo 600.000 ili više" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "Previsoko postavljen KDF-a kod starijih uređaja, može dovesti do problema s performansama. Povećaj vrijednost u $VALUE$ i testiraj svoj uređaj.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Za ponovno uspostavljanje pretplate, obratite se korisničkoj podršci." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domena potvrđena" }, + "itemAddedToFavorites": { + "message": "Stavka dodana u omiljene" + }, + "itemRemovedFromFavorites": { + "message": "Stavka uklonjenja iz omiljenih" + }, + "copyNote": { + "message": "Kopiraj bilješku" + }, "organizationNameMaxLength": { "message": "Naziv organizacije ne može biti duži od 50 znakova." }, @@ -11295,45 +11348,45 @@ "description": "Verb" }, "unArchive": { - "message": "Unarchive" + "message": "Poništi arhiviranje" }, "itemsInArchive": { - "message": "Items in archive" + "message": "Stavke u arhivi" }, "noItemsInArchive": { "message": "Nema stavki u arhivi" }, "noItemsInArchiveDesc": { - "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + "message": "Arhivirane stavke biti će prikazane ovdje i biti će izuzete iz rezultata općih pretraga i preporuka auto-ispune." }, "itemWasSentToArchive": { - "message": "Item was sent to archive" + "message": "Stavka poslana u arhivu" }, "itemsWereSentToArchive": { - "message": "Items were sent to archive" + "message": "Stavke poslane u arhivu" }, "itemUnarchived": { - "message": "Item was unarchived" + "message": "Stavka vraćena iz arhive" }, "bulkArchiveItems": { - "message": "Items archived" + "message": "Stavke arhivirane" }, "bulkUnarchiveItems": { - "message": "Items unarchived" + "message": "Stavke vraćene iz arhive" }, "archiveItem": { - "message": "Archive item", + "message": "Arhiviraj stavku", "description": "Verb" }, "archiveItemConfirmDesc": { - "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + "message": "Arhivirane stavke biti će izuzete iz rezultata općih pretraga i preporuka auto-ispune. Sigurno želiš arhivirati?" }, "archiveBulkItems": { - "message": "Archive items", + "message": "Arhiviraj stavke", "description": "Verb" }, "archiveBulkItemsConfirmDesc": { - "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive these items?" + "message": "Arhivirane stavke biti će izuzete iz rezultata općih pretraga i preporuka auto-ispune. Sigurno želiš arhivirati?" }, "businessUnit": { "message": "Poslovna jedinica" @@ -11837,24 +11890,51 @@ "message": "Nastavi bez nadogradnje" }, "upgradeYourPlan": { - "message": "Upgrade your plan" + "message": "Nadogradi svoj paket" }, "upgradeNow": { - "message": "Upgrade now" + "message": "Nadogradi sada" }, "formWillCreateNewFamiliesOrgMessage": { - "message": "Completing this form will create a new Families organization. You can upgrade your Free organization from the Admin Console." + "message": "Ispunjavanjem ovog obrasca stvorit ćeš novu obiteljsku organizaciju. Svoju besplatnu organizaciju možeš nadograditi u administratorskoj konzoli." }, "upgradeErrorMessage": { "message": "Došlo je do pogreške prilikom obrade nadogradnje. Pokušaj ponovno." }, "bitwardenFreeplanMessage": { - "message": "You have the Bitwarden Free plan" + "message": "Imaš besplatni Bitwarden paket" }, "upgradeCompleteSecurity": { - "message": "Upgrade for complete security" + "message": "Nadogradi za potpunu sigurnost" }, "viewbusinessplans": { - "message": "View business plans" + "message": "Pogledaj poslovne pakete" + }, + "updateEncryptionSettings": { + "message": "Ažuriraj postakve šifriranja" + }, + "updateYourEncryptionSettings": { + "message": "Ažuriraj svoje postakve šifriranja" + }, + "updateSettings": { + "message": "Ažuriraj postavke" + }, + "algorithm": { + "message": "Algoritam" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Odaberi kako Bitwarden treba šifrirati podatke tvojeg trezora. Sve opcije su sigurne, ali jače metode nude bolju zaštitu - posebno od napada grubom silom. Bitwarden preporučuje zadanu postavku za većinu korisnika." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Povećanje vrijednosti iznad zadanih poboljšat će sigurnost, ali će zbog toga otključavanje tvojeg trezora možda trajati dulje." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "O algoritmima šifriranja" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 je dobro testirana metoda šifriranja koja uravnotežuje sigurnost i performanse. Dobra je za sve korisnike." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id nudi jaču zaštitu od modernih napada. Najbolje za napredne korisnike s moćnim uređajima." } } diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index f9affbb4f9c..d299514da4c 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ új kockázatos jelszó", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Értesített tagok ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Kulcs beállítások titkosítása" }, - "kdfAlgorithm": { - "message": "KDF algoritmus" - }, "kdfIterations": { "message": "KDF iterációk" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "A magasabb szintű KDF iterációk, a memória és a párhuzamosság segíthet megvédeni mesterjelszót a támadók erőszakossága ellen." }, - "changeKdf": { - "message": "KDF megváltoztatása" - }, "encKeySettingsChanged": { "message": "A kulcs beállítás titkosítása megváltozott." }, @@ -5710,6 +5713,65 @@ "message": "Bővebben:", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Elérhető most" + }, + "autoConfirm": { + "message": "Új felhasználók automatikus megerősítése" + }, + "autoConfirmDescription": { + "message": "A szervezetbe meghívott új felhasználók automatikusan megerősítést kapnak, ha egy adminisztrátor eszköz feloldásra kerül.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "Hogyan kapcsolható be az automatikus felhasználó megerősítés" + }, + "autoConfirmStep1": { + "message": "Nyissuk meg a Bitwarden bővítményt." + }, + "autoConfirmStep2a": { + "message": "Kijelölés", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Bekapcsolás.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Sikeresen megnyitásra került a Bitwarden böngésző bővítmény. Most már aktiválhatjuk az automatikus felhasználó megerősítés beállítást." + }, + "autoConfirmPolicyEditDescription": { + "message": "A szervezetbe meghívott új felhasználók automatikusan megerősítést kapnak, ha egy adminisztrátor eszköz feloldásra kerül. Mielőtt bekapcsolnánk ezt a szabályzatot, tekintsük át és fogadjuk el a következőket: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potenciális biztonsági kockázat. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Az automatikus felhasználói megerőstíés biztonsági kockázatot jelenthet a szervezet adataira nézve." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "További információ a kockázatokról", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Az önálló szervezet irányelv nem engedélyezett. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Bárkinek, aki egynél több szervezet tagja, visszavonják a hozzáférését, amíg nem hagyja el a többi szervezetet." + }, + "autoConfirmSingleOrgExemption": { + "message": "Az egységes szervezeti irányelv minden szerepkörre kiterjed. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "Nincs vészhelyzeti hozzáférés. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "A vészhelyzeti hozzáférés eltávolításra került." + }, + "autoConfirmCheckBoxLabel": { + "message": "Elfogadom ezeket a kockázatokat és a szabályzat frissítéseit." + }, "personalOwnership": { "message": "Személyes tulajdon" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Ki" }, - "higherKDFIterations": { - "message": "A magasabb szintű KDF iterációk segíthetnek megvédeni mesterjelszót a támadók erőszakossága ellen." - }, - "incrementsOf100,000": { - "message": "100 000-es lépésekben" - }, - "smallIncrements": { - "message": "kis lépésekben" - }, "kdfIterationRecommends": { "message": "Célszerű 600 000 vagy nagyobb lépésben" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "Régebbi eszközökön a KDF túl magas beállítása teljesítmény problémákat okozhat. Növeljük az értéket $VALUE$ lépésbern és teszteljük az eszközöket.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Lépjünk kapcsolatba az ügyfélszolgálattal az előfizetés visszaállításához." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "A tartomány követelésre került." }, + "itemAddedToFavorites": { + "message": "Az elem bekerült a kedvencekhez.." + }, + "itemRemovedFromFavorites": { + "message": "Az elem eltávolítva a kedvencek közül." + }, + "copyNote": { + "message": "Jegyzet másolása" + }, "organizationNameMaxLength": { "message": "A szervezet neve nem haladhatja meg az 50 karaktert." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "Üzleti csomagok megtekintése" + }, + "updateEncryptionSettings": { + "message": "Titkosítási beállítások frissítése" + }, + "updateYourEncryptionSettings": { + "message": "Frissítsük a titkosítási beállításokat." + }, + "updateSettings": { + "message": "Beállítások frissítése" + }, + "algorithm": { + "message": "Algoritmus" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Válasszuk ki, hogy a Bitwarden hogyan titkosítsa a széf adatokat. Minden lopció biztonságos, de az erősebb módszerek jobb védelmet nyújtanak - különösen a brute force típusú támadások ellen. A Bitwarden a legtöbb felhasználó számára az alapértelmezett beállítást ajánlja." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Az értékek alapértelmezett fölé emelése javítja a biztonságot, de ennek eredményeként a széf feloldása hosszabb időt vehet igénybe." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "A titkosítási algoritmusokról" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "A PBKDF2-SHA256 egy jól tesztelt titkosítási módszer, amely egyensúlyban tartja a biztonságot és a teljesítményt. Minden felhasználó számára jó." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Az Argon2id erősebb védelmet nyújt a modern támadásokkal szemben. A legjobb a nagy teljesítményű eszközökkel rendelkező haladó felhasználók számára." } } diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index 9369bd3f564..352b88ce2d1 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Anggota yang diberitahukan ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Pengaturan Kunci Enkripsi" }, - "kdfAlgorithm": { - "message": "Algoritma KDF" - }, "kdfIterations": { "message": "Iterasi KDF" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Iterasi KDF, memori, dan paralelisme yang lebih tinggi dapat membantu melindungi sandi utama Anda dari serangan brute forced oleh penyerang." }, - "changeKdf": { - "message": "Ubah KDF" - }, "encKeySettingsChanged": { "message": "Pengaturan Kunci Enkripsi Berubah" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Kepemilikan Pribadi" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Off" }, - "higherKDFIterations": { - "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." - }, - "incrementsOf100,000": { - "message": "increments of 100,000" - }, - "smallIncrements": { - "message": "small increments" - }, "kdfIterationRecommends": { "message": "We recommend 600,000 or more" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "For older devices, setting your KDF too high may lead to performance issues. Increase the value in $VALUE$ and test your devices.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index 0bd1eb4a4e4..beb8e30f443 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -27,7 +27,7 @@ "message": "Controlla le password a rischio (deboli, esposte o riutilizzate). Seleziona le applicazioni critiche per determinare la priorità delle azioni di sicurezza." }, "dataLastUpdated": { - "message": "Ultimo aggiornamento: $DATE$", + "message": "Ultimo aggiornamento dei dati: $DATE$", "placeholders": { "date": { "content": "$1", @@ -36,7 +36,7 @@ } }, "noReportRan": { - "message": "Non hai ancora generato un report" + "message": "Non hai ancora generato un rapporto" }, "notifiedMembers": { "message": "Membri notificati" @@ -72,7 +72,7 @@ } }, "securityTasksCompleted": { - "message": "completate $COUNT$ attività relative alla sicurezza su $TOTAL$", + "message": "Completate $COUNT$ attività relative alla sicurezza su $TOTAL$", "placeholders": { "count": { "content": "$1", @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ nuove password a rischio", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Membri notificati ($COUNT$)", "placeholders": { @@ -191,7 +200,7 @@ "message": "Contrassegna l'elemento come critico" }, "applicationsSelected": { - "message": "applicazioni selezionate" + "message": "Applicazioni selezionate" }, "selectApplication": { "message": "Seleziona applicazione" @@ -224,7 +233,7 @@ "message": "Membri a rischio" }, "membersWithAccessToAtRiskItemsForCriticalApps": { - "message": "Membri con accesso ad elementi a rischio per applicazioni critiche" + "message": "Membri con accesso agli elementi a rischio per applicazioni critiche" }, "membersAtRiskCount": { "message": "$COUNT$ membri a rischio", @@ -254,19 +263,19 @@ } }, "atRiskMembersDescription": { - "message": "Questi membri accedono ad applicazioni con parole d'accesso deboli, esposte, o riutilizzate." + "message": "Questi membri accedono ad applicazioni con password deboli, esposte, o riutilizzate." }, "atRiskMembersDescriptionNone": { "message": "Non ci sono utenti connessi con password deboli, esposte o riutilizzate." }, "atRiskApplicationsDescription": { - "message": "Queste applicazioni hanno parole d'accesso deboli, esposte o riutilizzate." + "message": "Queste applicazioni hanno password deboli, esposte o riutilizzate." }, "atRiskApplicationsDescriptionNone": { "message": "Non ci sono applicazioni con password deboli, esposte o riutilizzate." }, "atRiskMembersDescriptionWithApp": { - "message": "Questi membri stanno entrando in $APPNAME$ con parole d'accesso deboli, esposte, o riutilizzate.", + "message": "Questi membri stanno entrando in $APPNAME$ con password deboli, esposte, o riutilizzate.", "placeholders": { "appname": { "content": "$1", @@ -473,7 +482,7 @@ "message": "Codice di sicurezza (CVV)" }, "securityCodeSlashCVV": { - "message": "Codice di sicurezza (CVV)" + "message": "Codice di sicurezza / CVV" }, "identityName": { "message": "Nome identità" @@ -1072,7 +1081,7 @@ "message": "Copia nome" }, "cardNumber": { - "message": "numero carta" + "message": "Numero carta" }, "copyFieldCipherName": { "message": "Copia $FIELD$, $CIPHERNAME$", @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Impostazioni chiave di crittografia" }, - "kdfAlgorithm": { - "message": "Algoritmo KDF" - }, "kdfIterations": { "message": "Iterazioni KDF" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Un numero di iterazioni KDF, memoria, e parallelismo più elevato può aiutare a proteggere la tua password principale dall'essere forzata da un utente malintenzionato." }, - "changeKdf": { - "message": "Cambia KDF" - }, "encKeySettingsChanged": { "message": "Impostazioni chiave di crittografia salvate" }, @@ -5710,6 +5713,65 @@ "message": "Scopri di più su ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Rimuovi cassaforte individuale" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Disattivo" }, - "higherKDFIterations": { - "message": "Un numero di iterazioni KDF più elevato può aiutare a proteggere la tua password principale dall'essere forzata da un attaccante." - }, - "incrementsOf100,000": { - "message": "incrementi di 100.000" - }, - "smallIncrements": { - "message": "piccoli incrementi" - }, "kdfIterationRecommends": { "message": "Consigliamo 600,000 o più" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "Per i dispositivi più vecchi, impostare il tuo KDF troppo alto potrebbe causare problemi di prestazioni. Aumenta il valore in $VALUE$ e prova i tuoi dispositivi.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contatta il Servizio Clienti per ripristinare il tuo abbonamento." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Dominio verificato" }, + "itemAddedToFavorites": { + "message": "Elemento aggiunto ai preferiti" + }, + "itemRemovedFromFavorites": { + "message": "Elemento rimosso dai preferiti" + }, + "copyNote": { + "message": "Copia nota" + }, "organizationNameMaxLength": { "message": "Il nome dell'organizzazione non può superare i 50 caratteri." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "Vedi i piani Business" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index 46ea6ff4433..09c3c0c5d5a 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "通知済みメンバー ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "暗号化の設定" }, - "kdfAlgorithm": { - "message": "KDFアルゴリズム" - }, "kdfIterations": { "message": "KDF反復回数" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "KDF のイテレーション、メモリ、並列性が高いほど、攻撃者によるマスターパスワードのブルートフォース攻撃を防止できます。" }, - "changeKdf": { - "message": "KDFの変更" - }, "encKeySettingsChanged": { "message": "暗号化キーの設定が変更されました" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "個別の保管庫を削除" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "オフ" }, - "higherKDFIterations": { - "message": "KDF 反復回数を多くすることで、攻撃者による総当たり攻撃からマスターパスワードを守ることができます。" - }, - "incrementsOf100,000": { - "message": "100,000回" - }, - "smallIncrements": { - "message": "少ない回数で" - }, "kdfIterationRecommends": { "message": "600,000以上がおすすめです" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "古いデバイスでは、KDF を高く設定するとパフォーマンスの問題が発生する可能性があります。 $VALUE$増やしてデバイスをテストしてください。", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " サブスクリプションを復活させるには、カスタマーサポートにご連絡ください。" }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index f8969bcd10a..c856c9b1b1d 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Encryption key settings" }, - "kdfAlgorithm": { - "message": "KDF algorithm" - }, "kdfIterations": { "message": "KDF iterations" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Higher KDF iterations, memory, and parallelism can help protect your master password from being brute forced by an attacker." }, - "changeKdf": { - "message": "Change KDF" - }, "encKeySettingsChanged": { "message": "Encryption key settings saved" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Off" }, - "higherKDFIterations": { - "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." - }, - "incrementsOf100,000": { - "message": "increments of 100,000" - }, - "smallIncrements": { - "message": "small increments" - }, "kdfIterationRecommends": { "message": "We recommend 600,000 or more" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "For older devices, setting your KDF too high may lead to performance issues. Increase the value in $VALUE$ and test your devices.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index af314c36f83..fc5efa82d50 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Encryption key settings" }, - "kdfAlgorithm": { - "message": "KDF algorithm" - }, "kdfIterations": { "message": "KDF iterations" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Higher KDF iterations, memory, and parallelism can help protect your master password from being brute forced by an attacker." }, - "changeKdf": { - "message": "Change KDF" - }, "encKeySettingsChanged": { "message": "Encryption key settings saved" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Off" }, - "higherKDFIterations": { - "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." - }, - "incrementsOf100,000": { - "message": "increments of 100,000" - }, - "smallIncrements": { - "message": "small increments" - }, "kdfIterationRecommends": { "message": "We recommend 600,000 or more" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "For older devices, setting your KDF too high may lead to performance issues. Increase the value in $VALUE$ and test your devices.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index 1620bb6a30e..ecb0ca92704 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "ಗೂಢ ಲಿಪೀಕರಣ ಕೀ ಸೆಟ್ಟಿಂಗ್‌ಗಳು" }, - "kdfAlgorithm": { - "message": "ಕೆಡಿಎಫ್ ಅಲ್ಗಾರಿದಮ್" - }, "kdfIterations": { "message": "ಕೆಡಿಎಫ್ ಪುನರಾವರ್ತನೆಗಳು" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Higher KDF iterations, memory, and parallelism can help protect your master password from being brute forced by an attacker." }, - "changeKdf": { - "message": "ಕೆಡಿಎಫ್ ಬದಲಾಯಿಸಿ" - }, "encKeySettingsChanged": { "message": "ಗೂಢ ಲಿಪೀಕರಣ ಕೀ ಸೆಟ್ಟಿಂಗ್‌ಗಳನ್ನು ಬದಲಾಯಿಸಲಾಗಿದೆ" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "ವೈಯಕ್ತಿಕ ಮಾಲೀಕತ್ವ" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Off" }, - "higherKDFIterations": { - "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." - }, - "incrementsOf100,000": { - "message": "increments of 100,000" - }, - "smallIncrements": { - "message": "small increments" - }, "kdfIterationRecommends": { "message": "We recommend 600,000 or more" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "For older devices, setting your KDF too high may lead to performance issues. Increase the value in $VALUE$ and test your devices.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index 49933357bc6..92b63d85c68 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "암호화 키 설정" }, - "kdfAlgorithm": { - "message": "KDF 알고리즘" - }, "kdfIterations": { "message": "KDF 이터레이션" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Higher KDF iterations, memory, and parallelism can help protect your master password from being brute forced by an attacker." }, - "changeKdf": { - "message": "KDF 변경" - }, "encKeySettingsChanged": { "message": "암호화 키 설정 변경됨" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "개인 소유권" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Off" }, - "higherKDFIterations": { - "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." - }, - "incrementsOf100,000": { - "message": "increments of 100,000" - }, - "smallIncrements": { - "message": "small increments" - }, "kdfIterationRecommends": { "message": "We recommend 600,000 or more" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "For older devices, setting your KDF too high may lead to performance issues. Increase the value in $VALUE$ and test your devices.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index f16be59a175..80fb4a98175 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -63,7 +63,7 @@ "message": "Izveidot jaunu pieteikšanās vienumu" }, "percentageCompleted": { - "message": "$PERCENT$% complete", + "message": "$PERCENT$% pabeigti", "placeholders": { "percent": { "content": "$1", @@ -72,7 +72,7 @@ } }, "securityTasksCompleted": { - "message": "$COUNT$ out of $TOTAL$ security tasks completed", + "message": "Pabeigti $COUNT$ no $TOTAL$ drošības uzdevumiem", "placeholders": { "count": { "content": "$1", @@ -85,16 +85,16 @@ } }, "passwordChangeProgress": { - "message": "Password change progress" + "message": "Paroles nomaiņas virzība" }, "assignMembersTasksToMonitorProgress": { - "message": "Assign members tasks to monitor progress" + "message": "Piešķirt dalībniekiem uzdevumus, lai pārraudzītu virzību" }, "onceYouReviewApps": { "message": "Tiklīdz lietotnes būs pārskatītas un atzīmētas kā būtiskas, dalībniekiem varēs piešķirt uzdevumus atrisināt riskam pakļautos vienumus un šeit pārskatīt virzību" }, "sendReminders": { - "message": "Send reminders" + "message": "Nosūtīt atgādinājumus" }, "onceYouMarkApplicationsCriticalTheyWillDisplayHere": { "message": "Tiklīz lietotnes tiks atzīmētas kā būtiskas, tās tiks parādītas šeit." @@ -146,7 +146,7 @@ } }, "countOfAtRiskPasswords": { - "message": "$COUNT$ passwords at-risk", + "message": "$COUNT$ paroles pakļautas riskam", "placeholders": { "count": { "content": "$1", @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ jaunas riskam pakļautas paroles", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Apziņotie dalībnieki ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Šifrēšanas atslēgas iestatījumi" }, - "kdfAlgorithm": { - "message": "KDF algoritms" - }, "kdfIterations": { "message": "KDF atkārtojumi" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Lielāks KDF atkārtojumu skaits, atmiņa un paralelitāte var palīdzēt aizsargāt galveno paroli pārlases uzbrukuma gadījumā." }, - "changeKdf": { - "message": "Mainīt KDF" - }, "encKeySettingsChanged": { "message": "Šifrēšanas atslēgas iestatījumi mainīti" }, @@ -5710,6 +5713,65 @@ "message": "Uzzināt vairāk par ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Pieejams tagad" + }, + "autoConfirm": { + "message": "Automātiska jaunu lietotāju apstiprināšana" + }, + "autoConfirmDescription": { + "message": "Jauni apvienībā uzaicinātie lietotāji tiks automātiski apstiprināti, kad pārvaldītāja ierīce tiks atslēgta.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "Kā ieslēgt automātisku lietotōtāju apstiprināšanu" + }, + "autoConfirmStep1": { + "message": "Jāatver Bitwarden paplašinājums." + }, + "autoConfirmStep2a": { + "message": "Jāatlasa", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": "“Ieslēgt”.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Bitwarden pārlūka paplašīnājums atvērts sekmīgi. Tagad var ieslēgt automātisko lietotāju apstiprināšanas iestatījumu." + }, + "autoConfirmPolicyEditDescription": { + "message": "Jauni apvienībā uzaicinātie lietotāji tiks automātiski apstiprināti, kad pārvaldītāja ierīce tiks atslēgta. Pirms šīs pamatnostādnes ieslēgšanas lūgums pārskatīt un piekrist šim: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Iespējams drošības risks. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automātiska lietotāju apstiprināšana var radīt drošības risku apvienības datiem." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Uzzināt par riskiem", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Nepieciešama vienas vienīgas apvienības pamatnostādne. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Ikvienam, kurš ir daļa no vairāk kā vienas apvienības, tiks atsaukta piekļuve, līdz tiks pamestas pārējās apvienības." + }, + "autoConfirmSingleOrgExemption": { + "message": "Vienas vienīgas apvienības pamatnostādne paplašināsies līdz visām lomām. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "Nav ārkārtas piekļuves. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Ārkārtas piekļuve tiks noņemta." + }, + "autoConfirmCheckBoxLabel": { + "message": "Es pieņemu šos riskus un pamatnostādnes atjauninājumus" + }, "personalOwnership": { "message": "Personīgās īpašumtiesības" }, @@ -9617,7 +9679,7 @@ "message": "Piešķirt" }, "assignTasks": { - "message": "Assign tasks" + "message": "Piešķirt uzdevumus" }, "assignToCollections": { "message": "Piešķirt krājumiem" @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Izslēgts" }, - "higherKDFIterations": { - "message": "Lielāks KDF atkārtojumu skaits var palīdzēt aizsargāt galveno paroli pārlases uzbrukuma gadījumā." - }, - "incrementsOf100,000": { - "message": "palielinājumiem par 100 000" - }, - "smallIncrements": { - "message": "nelieliem palielinājumiem" - }, "kdfIterationRecommends": { "message": "Mēs iesakām 600 000 vai vairāk" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "Pārāk augsta KDF iestatīšana vecākās ierīcēs var novest pie veiktspējas sarežģījumiem. Jāpalielina vērtība ar $VALUE$ un jāpārbauda savas ierīces.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Jāsazinās ar klientu atbalstu, lai atjaunotu Tavu abonementu." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domēns pieteikts" }, + "itemAddedToFavorites": { + "message": "Vienums pievienots izlasei" + }, + "itemRemovedFromFavorites": { + "message": "Vienums noņemts no izlases" + }, + "copyNote": { + "message": "Ievietot piezīmi starpliktuvē" + }, "organizationNameMaxLength": { "message": "Apvienības nosaukums nevar pārsniegt 50 rakstzīmes." }, @@ -11287,11 +11340,11 @@ "message": "Meklēt arhīvā" }, "archiveNoun": { - "message": "Archive", + "message": "Arhīvs", "description": "Noun" }, "archiveVerb": { - "message": "Archive", + "message": "Arhivēt", "description": "Verb" }, "unArchive": { @@ -11316,10 +11369,10 @@ "message": "Vienums tika izņemts no arhīva" }, "bulkArchiveItems": { - "message": "Items archived" + "message": "Vienumi tika arhivēti" }, "bulkUnarchiveItems": { - "message": "Items unarchived" + "message": "Vienumi tika izņemti no arhīva" }, "archiveItem": { "message": "Arhivēt vienumu", @@ -11443,10 +11496,10 @@ "message": "Bitwarden paplašinājums uzstādīts." }, "openTheBitwardenExtension": { - "message": "Open the Bitwarden extension" + "message": "Atvērt Bitwarden paplašinājumu" }, "bitwardenExtensionInstalledOpenExtension": { - "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + "message": "Bitwarden paplašinājums ir uzstādīts. Atver paplašinājumu, lai pieteiktos un sāktu automātsko aizpildīšanu!" }, "openExtensionToAutofill": { "message": "Jāatver paplašinājums, lai pieteiktos un uzsāktu automātisko aizpildi." @@ -11616,7 +11669,7 @@ "message": "Trūkst nodokļu Id" }, "missingTaxIdWarning": { - "message": "Action required: You're missing a Tax ID number in payment details. If a Tax ID is not added, your invoices may include additional tax." + "message": "Nepieciešama darbība: maksājuma informācijā trūkst nodokļu identifikatora numura. Ja nodokļu identifikators nav pievienots, rēķinos var tikt iekļauts papildu nodoklis." }, "moreBreadcrumbs": { "message": "Vairāk norāžu", @@ -11626,19 +11679,19 @@ "message": "Pievienot nodokļu Id" }, "missingTaxIdCalloutTitle": { - "message": "Action required: Missing Tax ID" + "message": "Nepieciešama darbība: trūkst nodokļu identifikatora" }, "missingTaxIdCalloutDescription": { - "message": "If a Tax ID is not added, your invoices may include additional tax." + "message": "Ja nodokļu identifikators nav pievienots, rēķinos var tikt iekļauts papildu nodoklis." }, "unverifiedTaxIdWarning": { - "message": "Action required: Your Tax ID number is unverified. If your Tax ID is left unverified, your invoices may include additional tax." + "message": "Nepieciešama darbība: nodokļu identifikatora numurs nav apliecināts. Ja nodokļu identifikators netiek apliecināts, rēķinos var tikt iekļauts papildu nodoklis." }, "editTaxId": { "message": "Labot savu nodokļu Id" }, "unverifiedTaxIdCalloutTitle": { - "message": "Tax ID unverified" + "message": "Nodokļu identifikators nav apliecināts" }, "unverifiedTaxIdCalloutDescription": { "message": "Check your Tax ID to verify the format is correct and there are no typos." @@ -11819,22 +11872,22 @@ "message": "Konts tika uzlabots uz ģimeņu plānu." }, "taxCalculationError": { - "message": "There was an error calculating tax for your location. Please try again." + "message": "Bija kļūda Tavas atrašanās vietas nodokļu aprēķināšanā. Lūgums mēģināt vēlreiz." }, "individualUpgradeWelcomeMessage": { - "message": "Welcome to Bitwarden" + "message": "Laipni lūdzam Bitwarden" }, "individualUpgradeDescriptionMessage": { - "message": "Unlock more security features with Premium, or start sharing items with Families" + "message": "Atslēdz vairāk drošības iespēju ar “Premium” vai uzsāc vienumu kopīgošanu ar plānu ģimenēm" }, "individualUpgradeTaxInformationMessage": { - "message": "Prices exclude tax and are billed annually." + "message": "Cenās nav iekļauts nodoklis, un maksa tiek ieturēta reizi gadā." }, "organizationNameDescription": { - "message": "Your organization name will appear in invitations you send to members." + "message": "Apvienības nosaukums parādīsies dalībniekiem nosūtītajos uzaicinājumos." }, "continueWithoutUpgrading": { - "message": "Continue without upgrading" + "message": "Turpināt bez uzlabošanas" }, "upgradeYourPlan": { "message": "Uzlabot savu plānu" @@ -11846,7 +11899,7 @@ "message": "Šīs veidlapas pabeigšana izveidos jaunu ģimeņu apvienību. Uzlabot savu bezmaksas apvienību var pārvaldības konsolē." }, "upgradeErrorMessage": { - "message": "We encountered an error while processing your upgrade. Please try again." + "message": "Mēs saskārāmies ar kļūdu uzlabošanas apstrādes laikā. Lūgums mēģināt vēlreiz." }, "bitwardenFreeplanMessage": { "message": "Tev ir Bitwarden bezmaksas plāns" @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "Apskatīt uzņēmējdarbības plānus" + }, + "updateEncryptionSettings": { + "message": "Atjaunināt šifrēšanas iestatījumus" + }, + "updateYourEncryptionSettings": { + "message": "Atjaunini savus šifrēšanas iestatījumus" + }, + "updateSettings": { + "message": "Atjaunināt Iestatījumus" + }, + "algorithm": { + "message": "Algoritms" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Jāizvēlas, kā Bitwarden vajadzētu šifrēt glabātavas datus. Visas iespējas ir drošanas, bet spēcīgāki veidi sniedz labāku aizsardzību ‒ jo īpaši pret pārlases uzbrukumiem. Bitwarden vairumam lietotāju iesaka noklusējuma iestatījumu." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Vērtību palielināšana virs noklusējuma uzlabos drošību, bet iznākumā var būt nepieciešams ilgāks laiks glabātavas atslēgšanai." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "Par šifrēšanas algoritmiem" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 ir labi pārbaudīts šifrēšanas veids, kas salāgo drošību un veiktspēju. Labs visiem lietotājiem." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id sniedz spēcīgāku aizsardzību pret mūsdienu uzbrukumiem. Vislabāk piemērots lietpratējiem ar jaudīgām ierīcēm." } } diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index d400df253e8..4f77abe98ba 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "എൻക്രിപ്ഷൻ കീയുടെ ക്രമീകരണങ്ങൾ" }, - "kdfAlgorithm": { - "message": "KDF അൽഗോരിതം" - }, "kdfIterations": { "message": "KDF Iterations" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Higher KDF iterations, memory, and parallelism can help protect your master password from being brute forced by an attacker." }, - "changeKdf": { - "message": "KDF മാറ്റുക" - }, "encKeySettingsChanged": { "message": "എൻക്രിപ്ഷൻ കീയുടെ ക്രമീകരണങ്ങൾ മാറ്റി" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "വ്യക്തിഗത ഉടമസ്ഥാവകാശം" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Off" }, - "higherKDFIterations": { - "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." - }, - "incrementsOf100,000": { - "message": "increments of 100,000" - }, - "smallIncrements": { - "message": "small increments" - }, "kdfIterationRecommends": { "message": "We recommend 600,000 or more" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "For older devices, setting your KDF too high may lead to performance issues. Increase the value in $VALUE$ and test your devices.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index 69dabfbe9ad..a9639023b9d 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Encryption key settings" }, - "kdfAlgorithm": { - "message": "KDF algorithm" - }, "kdfIterations": { "message": "KDF iterations" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Higher KDF iterations, memory, and parallelism can help protect your master password from being brute forced by an attacker." }, - "changeKdf": { - "message": "Change KDF" - }, "encKeySettingsChanged": { "message": "Encryption key settings saved" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Off" }, - "higherKDFIterations": { - "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." - }, - "incrementsOf100,000": { - "message": "increments of 100,000" - }, - "smallIncrements": { - "message": "small increments" - }, "kdfIterationRecommends": { "message": "We recommend 600,000 or more" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "For older devices, setting your KDF too high may lead to performance issues. Increase the value in $VALUE$ and test your devices.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index af314c36f83..fc5efa82d50 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Encryption key settings" }, - "kdfAlgorithm": { - "message": "KDF algorithm" - }, "kdfIterations": { "message": "KDF iterations" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Higher KDF iterations, memory, and parallelism can help protect your master password from being brute forced by an attacker." }, - "changeKdf": { - "message": "Change KDF" - }, "encKeySettingsChanged": { "message": "Encryption key settings saved" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Off" }, - "higherKDFIterations": { - "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." - }, - "incrementsOf100,000": { - "message": "increments of 100,000" - }, - "smallIncrements": { - "message": "small increments" - }, "kdfIterationRecommends": { "message": "We recommend 600,000 or more" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "For older devices, setting your KDF too high may lead to performance issues. Increase the value in $VALUE$ and test your devices.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index a37fb08322f..348ef01c2a8 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Innstillinger for krypteringsnøkkel" }, - "kdfAlgorithm": { - "message": "KDF-algoritme" - }, "kdfIterations": { "message": "KDF-iterasjoner" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Høyere KDF-iterasjoner, minne og parallellitet kan hjelpe til med å beskytte hovedpassordet ditt fra å bli gjettet med rå kraft av en angriper." }, - "changeKdf": { - "message": "Endre KDF" - }, "encKeySettingsChanged": { "message": "Krypteringsnøkkelinnstillingene endret" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Personlig eierskap" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Av" }, - "higherKDFIterations": { - "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." - }, - "incrementsOf100,000": { - "message": "increments of 100,000" - }, - "smallIncrements": { - "message": "small increments" - }, "kdfIterationRecommends": { "message": "We recommend 600,000 or more" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "For older devices, setting your KDF too high may lead to performance issues. Increase the value in $VALUE$ and test your devices.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index 477ee9e1150..d069e4a6e9c 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Encryption key settings" }, - "kdfAlgorithm": { - "message": "KDF algorithm" - }, "kdfIterations": { "message": "KDF iterations" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Higher KDF iterations, memory, and parallelism can help protect your master password from being brute forced by an attacker." }, - "changeKdf": { - "message": "Change KDF" - }, "encKeySettingsChanged": { "message": "Encryption key settings saved" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Off" }, - "higherKDFIterations": { - "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." - }, - "incrementsOf100,000": { - "message": "increments of 100,000" - }, - "smallIncrements": { - "message": "small increments" - }, "kdfIterationRecommends": { "message": "We recommend 600,000 or more" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "For older devices, setting your KDF too high may lead to performance issues. Increase the value in $VALUE$ and test your devices.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index 8891df75c62..93334ff9e3c 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ nieuwe wachtwoorden lopen risico", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Geînformeerde leden ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Instellingen encryptiesleutel" }, - "kdfAlgorithm": { - "message": "KDF-algortime" - }, "kdfIterations": { "message": "KDF-iteraties" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Hogere instellingen voor KDF-iteraties, geheugen en parallellisatie kunnen je hoofdwachtwoord beschermen tegen brute-foce-aanvallen." }, - "changeKdf": { - "message": "KDF wijzigen" - }, "encKeySettingsChanged": { "message": "Instellingen encryptiesleutel zijn gewijzigd" }, @@ -5710,6 +5713,65 @@ "message": "Leer meer over de ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Nu beschikbaar" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Inschakelen.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Mogelijk beveiligingsrisico. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Persoonlijk eigendom" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Uit" }, - "higherKDFIterations": { - "message": "Hogere KDF-iteraties beschermen je hoofdwachtwoord tegen brute-foce-aanvallen." - }, - "incrementsOf100,000": { - "message": "verhogingen van 100.000" - }, - "smallIncrements": { - "message": "kleine verhogingen" - }, "kdfIterationRecommends": { "message": "We adviseren 600.000 of meer" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "Voor oudere apparaten kan het te hoog instellen van je KDF tot prestatieproblemen leiden. Verhoog de waarde in $VALUE$ en test je apparaten.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Neem contact op met de klantenservice om je abonnement te herstellen." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domein geverifieerd" }, + "itemAddedToFavorites": { + "message": "Item toegevoegd aan favorieten" + }, + "itemRemovedFromFavorites": { + "message": "Item verwijderd uit favorieten" + }, + "copyNote": { + "message": "Notitie kopiëren" + }, "organizationNameMaxLength": { "message": "Organisatienaam mag niet langer zijn dan 50 tekens." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "Bedrijfsabonnementen bekijken" + }, + "updateEncryptionSettings": { + "message": "Versleutelingsinstellingen bijwerken" + }, + "updateYourEncryptionSettings": { + "message": "Je versleutelingsinstellingen bijwerken" + }, + "updateSettings": { + "message": "Instellingen bijwerken" + }, + "algorithm": { + "message": "Algoritme" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Kies hoe Bitwarden je kluisgegevens moet versleutelen. Alle opties zijn veilig, maar sterkere methoden bieden betere bescherming - vooral tegen brute-force aanvallen. Bitwarden beveelt de standaardinstelling voor de meeste gebruikers aan." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Het verhogen van de waarden boven de standaard zal de veiligheid verbeteren, maar het ontgrendelen van je kluis kan langer duren." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "Over versleutelingsalgoritmen" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is een goed geteste versleutelingsmethode met evenwicht tussen veiligheid en prestaties. Goed voor alle gebruikers." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id biedt een sterkere bescherming tegen moderne aanvallen. Het beste voor geavanceerde gebruikers met krachtige apparaten." } } diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index 6c349c29ff0..fdc9eb8127f 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Encryption key settings" }, - "kdfAlgorithm": { - "message": "KDF algorithm" - }, "kdfIterations": { "message": "KDF iterations" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Higher KDF iterations, memory, and parallelism can help protect your master password from being brute forced by an attacker." }, - "changeKdf": { - "message": "Change KDF" - }, "encKeySettingsChanged": { "message": "Encryption key settings saved" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Off" }, - "higherKDFIterations": { - "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." - }, - "incrementsOf100,000": { - "message": "increments of 100,000" - }, - "smallIncrements": { - "message": "small increments" - }, "kdfIterationRecommends": { "message": "We recommend 600,000 or more" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "For older devices, setting your KDF too high may lead to performance issues. Increase the value in $VALUE$ and test your devices.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index af314c36f83..fc5efa82d50 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Encryption key settings" }, - "kdfAlgorithm": { - "message": "KDF algorithm" - }, "kdfIterations": { "message": "KDF iterations" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Higher KDF iterations, memory, and parallelism can help protect your master password from being brute forced by an attacker." }, - "changeKdf": { - "message": "Change KDF" - }, "encKeySettingsChanged": { "message": "Encryption key settings saved" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Off" }, - "higherKDFIterations": { - "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." - }, - "incrementsOf100,000": { - "message": "increments of 100,000" - }, - "smallIncrements": { - "message": "small increments" - }, "kdfIterationRecommends": { "message": "We recommend 600,000 or more" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "For older devices, setting your KDF too high may lead to performance issues. Increase the value in $VALUE$ and test your devices.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index 1f48e445f7f..82bb4a85cbb 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Powiadomieni członkowie ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Ustawienia klucza szyfrowania" }, - "kdfAlgorithm": { - "message": "Algorytm KDF" - }, "kdfIterations": { "message": "Iteracje KDF" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Wyższe wartości iteracji KDF, pamięci i współbieżności mogą pomóc chronić Twoje hasło główne przed złamaniem przez atakującego." }, - "changeKdf": { - "message": "Zmień KDF" - }, "encKeySettingsChanged": { "message": "Ustawienia klucza szyfrowania zostały zapisane" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Własność osobista" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Wył." }, - "higherKDFIterations": { - "message": "Wyższe wartości iteracji KDF mogą pomóc chronić Twoje hasło główne przed złamaniem przez atakującego." - }, - "incrementsOf100,000": { - "message": "zwiększa o 100 000" - }, - "smallIncrements": { - "message": "małe przyrosty" - }, "kdfIterationRecommends": { "message": "Zalecamy 600 000 lub więcej" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "Dla starszych urządzeń, ustawienie KDF zbyt wysokie może prowadzić do problemów z wydajnością. Zwiększ wartość o $VALUE$ i przetestuj swoje urządzenia.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Skontaktuj się z działem obsługi klienta w celu przywrócenia subskrypcji." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domena zgłoszona" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Nazwa organizacji nie może przekraczać 50 znaków." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index e87403000ae..61296561fe6 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Membros notificados ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Configurações da chave de criptografia" }, - "kdfAlgorithm": { - "message": "Algoritmo da KDF" - }, "kdfIterations": { "message": "Iterações da KDF" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Mais iterações KDF, memória e paralelismo podem ajudar a proteger sua senha mestre de ser descoberta por força bruta por um invasor." }, - "changeKdf": { - "message": "Alterar KDF" - }, "encKeySettingsChanged": { "message": "As configurações da chave de criptografia foram salvas" }, @@ -5710,6 +5713,65 @@ "message": "Saiba mais sobre o ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Remover cofre individual" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Desligado" }, - "higherKDFIterations": { - "message": "Iterações KDF mais altas podem ajudar a proteger sua senha mestra de ser descoberta por força bruta por alguém mal-intencionado." - }, - "incrementsOf100,000": { - "message": "incrementos de 100.000" - }, - "smallIncrements": { - "message": "pequenos incrementos" - }, "kdfIterationRecommends": { "message": "Recomendamos 600.000 ou mais" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "Para dispositivos mais antigos, configurar seu KDF muito alto pode causar problemas de desempenho. Aumente o valor em $VALUE$ e teste seus dispositivos.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contate o Atendimento ao Cliente para restabelecer sua assinatura." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domínio reivindicado" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "O nome da organização não pode exceder 50 caracteres." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index b4d0e2b5a6a..321beaf907d 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ novas palavras-passe em risco", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Membros notificados ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Definições da chave de encriptação" }, - "kdfAlgorithm": { - "message": "Algoritmo KDF" - }, "kdfIterations": { "message": "Iterações KDF" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Iterações KDF mais altas, memória e paralelismo podem ajudar a proteger a sua palavra-passe mestra de ser forçada por um atacante." }, - "changeKdf": { - "message": "Alterar KDF" - }, "encKeySettingsChanged": { "message": "Definições da chave de encriptação alteradas" }, @@ -5710,6 +5713,65 @@ "message": "Saiba mais sobre o ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Remover cofre pessoal" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Desativado" }, - "higherKDFIterations": { - "message": "Iterações KDF mais altas podem ajudar a proteger a sua palavra-passe mestra de ser forçada por um atacante." - }, - "incrementsOf100,000": { - "message": "incrementos de 100.000" - }, - "smallIncrements": { - "message": "pequenos incrementos" - }, "kdfIterationRecommends": { "message": "Recomendamos 600.000 ou mais" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "Para dispositivos mais antigos, definir um KDF demasiado alto pode levar a problemas de desempenho. Aumente o valor em $VALUE$ e teste os seus dispositivos.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contacte o Apoio ao Cliente para restabelecer a sua subscrição." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domínio reivindicado" }, + "itemAddedToFavorites": { + "message": "Item adicionado aos favoritos" + }, + "itemRemovedFromFavorites": { + "message": "Item removido dos favoritos" + }, + "copyNote": { + "message": "Copiar nota" + }, "organizationNameMaxLength": { "message": "O nome da organização não pode exceder 50 caracteres." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "Ver planos empresariais" + }, + "updateEncryptionSettings": { + "message": "Atualizar definições de encriptação" + }, + "updateYourEncryptionSettings": { + "message": "Atualize as suas definições de encriptação" + }, + "updateSettings": { + "message": "Atualizar definições" + }, + "algorithm": { + "message": "Algoritmo" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Escolha como o Bitwarden deve encriptar os dados do seu cofre. Todas as opções são seguras, mas métodos mais fortes oferecem melhor proteção, especialmente contra ataques de força bruta. O Bitwarden recomenda a configuração predefinida para a maioria dos utilizadores." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Aumentar os valores acima do predefinido melhorará a segurança, mas o seu cofre poderá demorar mais tempo a desbloquear como resultado." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "Sobre algoritmos de encriptação" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "O PBKDF2-SHA256 é um método de encriptação bem testado que equilibra segurança e desempenho. Adequado para todos os utilizadores." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "O Argon2id oferece uma proteção mais forte contra ataques modernos. Ideal para utilizadores avançados com dispositivos potentes." } } diff --git a/apps/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index cee47198bf7..f60f94498f1 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Setări cheie de criptare" }, - "kdfAlgorithm": { - "message": "Algoritm KDF" - }, "kdfIterations": { "message": "Iterații KDF" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Higher KDF iterations, memory, and parallelism can help protect your master password from being brute forced by an attacker." }, - "changeKdf": { - "message": "Modificare KDF" - }, "encKeySettingsChanged": { "message": "Setările cheii de criptare salvate" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Înlăturați seiful personal" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Off" }, - "higherKDFIterations": { - "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." - }, - "incrementsOf100,000": { - "message": "increments of 100,000" - }, - "smallIncrements": { - "message": "small increments" - }, "kdfIterationRecommends": { "message": "We recommend 600,000 or more" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "For older devices, setting your KDF too high may lead to performance issues. Increase the value in $VALUE$ and test your devices.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index b061ba2f652..fe47aff8f39 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Уведомленные участники ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Настройки ключа шифрования" }, - "kdfAlgorithm": { - "message": "Алгоритм KDF" - }, "kdfIterations": { "message": "Итерации KDF" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Увеличение числа итераций KDF, памяти и параллелизма может помочь защитить ваш мастер-пароль от взлома его злоумышленником." }, - "changeKdf": { - "message": "Изменить KDF" - }, "encKeySettingsChanged": { "message": "Настройки ключа шифрования сохранены" }, @@ -5710,6 +5713,65 @@ "message": "Узнайте больше о ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Уже доступно" + }, + "autoConfirm": { + "message": "Автоматическое подтверждение новых пользователей" + }, + "autoConfirmDescription": { + "message": "Новые пользователи, приглашенные в организацию, будут автоматически подтверждены, когда устройство администратора будет разблокировано.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "Как включить автоматическое подтверждение пользователя" + }, + "autoConfirmStep1": { + "message": "Откройте свое расширение Bitwarden." + }, + "autoConfirmStep2a": { + "message": "Выбрать", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Включить.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Успешно открыто расширение браузера Bitwarden. Теперь вы можете активировать автоматическое подтверждение пользователя." + }, + "autoConfirmPolicyEditDescription": { + "message": "Новые пользователи, приглашенные в организацию, будут автоматически подтверждены, когда устройство администратора будет разблокировано. Перед включением этой политики, пожалуйста, ознакомьтесь со следующими условиями и согласитесь с ними: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Потенциальные риски безопасности. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Автоматическое подтверждение пользователя может представлять угрозу безопасности данных вашей организации." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Узнайте о рисках", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Требуется политика единой организации. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "У любого сотрудника, работающего в нескольких организациях, будет отозван доступ до тех пор, пока он не покинет другие организации." + }, + "autoConfirmSingleOrgExemption": { + "message": "Политика единой организации распространяется на все роли. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "Нет экстренного доступа. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Экстренный доступ будет удален." + }, + "autoConfirmCheckBoxLabel": { + "message": "Я принимаю эти риски и политики обновления" + }, "personalOwnership": { "message": "Удалить личное хранилище" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Выкл" }, - "higherKDFIterations": { - "message": "Увеличение числа итераций KDF может помочь защитить ваш мастер-пароль от взлома его злоумышленником." - }, - "incrementsOf100,000": { - "message": "с шагом 100 000" - }, - "smallIncrements": { - "message": "небольшие приращения" - }, "kdfIterationRecommends": { "message": "Мы рекомендуем 600000 или более" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "Для устаревших устройств слишком высокое значение KDF может привести к проблемам с производительностью. Увеличьте значение до $VALUE$ и протестируйте свои устройства.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Обратитесь в службу поддержки клиентов, чтобы восстановить подписку." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Домен зарегистрирован" }, + "itemAddedToFavorites": { + "message": "Элемент добавлен в избранное" + }, + "itemRemovedFromFavorites": { + "message": "Элемент удален из избранного" + }, + "copyNote": { + "message": "Скопировать заметку" + }, "organizationNameMaxLength": { "message": "Название организации не может превышать 50 символов." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Обновить настройки шифрования" + }, + "updateYourEncryptionSettings": { + "message": "Обновите настройки шифрования" + }, + "updateSettings": { + "message": "Обновить настройки" + }, + "algorithm": { + "message": "Алгоритм" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Выберите, каким образом Bitwarden должен шифровать данные вашего хранилища. Все варианты защищены, но более надежные методы обеспечивают лучшую защиту, особенно от атак методом перебора. Большинству пользователей Bitwarden рекомендует использовать настройки по умолчанию." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Увеличение значений выше значений по умолчанию повысит защищенность, но в итоге разблокировка вашего хранилища может занять больше времени." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "Об алгоритмах шифрования" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 - это хорошо протестированный метод шифрования, который обеспечивает баланс между безопасностью и производительностью. Подходит для всех пользователей." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id обеспечивает более надежную защиту от современных атак. Лучше всего подходит для опытных пользователей с мощными устройствами." } } diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index 8681e95cf79..1a6dff74e32 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Encryption key settings" }, - "kdfAlgorithm": { - "message": "KDF algorithm" - }, "kdfIterations": { "message": "KDF iterations" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Higher KDF iterations, memory, and parallelism can help protect your master password from being brute forced by an attacker." }, - "changeKdf": { - "message": "Change KDF" - }, "encKeySettingsChanged": { "message": "Encryption key settings saved" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Off" }, - "higherKDFIterations": { - "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." - }, - "incrementsOf100,000": { - "message": "increments of 100,000" - }, - "smallIncrements": { - "message": "small increments" - }, "kdfIterationRecommends": { "message": "We recommend 600,000 or more" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "For older devices, setting your KDF too high may lead to performance issues. Increase the value in $VALUE$ and test your devices.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index 160afd0083b..1a5e3b3c020 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -91,7 +91,7 @@ "message": "Pre sledovanie progresu, priraďte členom úlohy" }, "onceYouReviewApps": { - "message": "Once you review applications and mark them as critical, you can assign tasks to members to resolve at-risk items and monitor progress here" + "message": "Keď skontrolujete a označíte kritické aplikácie, môžete na tomto mieste prideliť členom úlohy pre ohrozené položky a sledovať progres" }, "sendReminders": { "message": "Poslať upomienky" @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ nových ohrozených hesiel", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Nastavenia šifrovacieho kľúča" }, - "kdfAlgorithm": { - "message": "KDF algoritmus" - }, "kdfIterations": { "message": "KDF iterácií" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Zvýšenie počtu KDF iterácií, pamäti a paralelizmu môže pomôcť chrániť vaše hlavné heslo pri brute force útoku." }, - "changeKdf": { - "message": "Zmeniť KDF" - }, "encKeySettingsChanged": { "message": "Nastavenia šifrovacieho kľúča zmenené" }, @@ -5710,6 +5713,65 @@ "message": "Viac informácií o ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Zakázať osobný trezor" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Vypnuté" }, - "higherKDFIterations": { - "message": "Zvýšenie počtu KDF iterácií môže pomôcť chrániť vaše hlavné heslo pri brute force útoku." - }, - "incrementsOf100,000": { - "message": "prírastok po 100 000" - }, - "smallIncrements": { - "message": "malý prírastok" - }, "kdfIterationRecommends": { "message": "Odporúčame aspoň 600 000" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "Nastavenie príliš vysokého počtu KDF iterácii môže viesť k problémom vo výkone na starších zariadeniach. Zvýšte hodnotu $VALUE$ a otestujte na vašich zariadeniach.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Ak chcete obnoviť vaše predplatné, kontaktujte zákaznícku podporu." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Doména privlastnená" }, + "itemAddedToFavorites": { + "message": "Položka pridaná medzi obľúbené" + }, + "itemRemovedFromFavorites": { + "message": "Položka odobraná z obľúbených" + }, + "copyNote": { + "message": "Kopírovať poznámku" + }, "organizationNameMaxLength": { "message": "Meno organizácie nemôže mať viac ako 50 znakov." }, @@ -11295,45 +11348,45 @@ "description": "Verb" }, "unArchive": { - "message": "Unarchive" + "message": "Zrušiť archiváciu" }, "itemsInArchive": { - "message": "Items in archive" + "message": "Položky v archíve" }, "noItemsInArchive": { "message": "Žiadne položky v archíve" }, "noItemsInArchiveDesc": { - "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." + "message": "Tu sa zobrazia archivované položky, ktoré budú vylúčené zo všeobecného vyhľadávania a z návrhov automatického vypĺňania." }, "itemWasSentToArchive": { - "message": "Item was sent to archive" + "message": "Položka bola archivovaná" }, "itemsWereSentToArchive": { - "message": "Items were sent to archive" + "message": "Položky boli archivované" }, "itemUnarchived": { - "message": "Item was unarchived" + "message": "Položka bola odobraná z archívu" }, "bulkArchiveItems": { - "message": "Items archived" + "message": "Položky archivované" }, "bulkUnarchiveItems": { - "message": "Items unarchived" + "message": "Položky boli odobrané z archívu" }, "archiveItem": { - "message": "Archive item", + "message": "Archivovať položku", "description": "Verb" }, "archiveItemConfirmDesc": { - "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + "message": "Archivované položky sú vylúčené zo všeobecného vyhľadávania a z návrhov automatického vypĺňania. Naozaj chcete archivovať túto položku?" }, "archiveBulkItems": { - "message": "Archive items", + "message": "Archivovať položky", "description": "Verb" }, "archiveBulkItemsConfirmDesc": { - "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive these items?" + "message": "Archivované položky sú vylúčené zo všeobecného vyhľadávania a z návrhov automatického vypĺňania. Naozaj chcete archivovať tieto položky?" }, "businessUnit": { "message": "Organizačná jednotka" @@ -11837,24 +11890,51 @@ "message": "Pokračovať bez povýšenia" }, "upgradeYourPlan": { - "message": "Upgrade your plan" + "message": "Navýšte si svoje predplatné" }, "upgradeNow": { - "message": "Upgrade now" + "message": "Navýšiť teraz" }, "formWillCreateNewFamiliesOrgMessage": { - "message": "Completing this form will create a new Families organization. You can upgrade your Free organization from the Admin Console." + "message": "Vyplnením tohto formulára sa vytvorí organizácia Rodiny. Povýšiť vašu organizáciu Zdarma môžete v konzole správcu." }, "upgradeErrorMessage": { "message": "Pri spracovaní povýšenia došlo k chybe. Prosím skúste to znova." }, "bitwardenFreeplanMessage": { - "message": "You have the Bitwarden Free plan" + "message": "Mate predplatné Bitwarden Zadarmo" }, "upgradeCompleteSecurity": { - "message": "Upgrade for complete security" + "message": "Navýšte predplatné pre kompletnú bezpečnosť" }, "viewbusinessplans": { - "message": "View business plans" + "message": "Zobraziť predplatné pre firmy" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index 3c478d03339..3b29b6eba50 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Nastavitve kodirnega ključa" }, - "kdfAlgorithm": { - "message": "Algoritem KDF" - }, "kdfIterations": { "message": "Ponovitev KDF" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Higher KDF iterations, memory, and parallelism can help protect your master password from being brute forced by an attacker." }, - "changeKdf": { - "message": "Spremeni KDF" - }, "encKeySettingsChanged": { "message": "Encryption key settings saved" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Off" }, - "higherKDFIterations": { - "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." - }, - "incrementsOf100,000": { - "message": "increments of 100,000" - }, - "smallIncrements": { - "message": "small increments" - }, "kdfIterationRecommends": { "message": "We recommend 600,000 or more" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "For older devices, setting your KDF too high may lead to performance issues. Increase the value in $VALUE$ and test your devices.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index bf47c17694c..f11b0954525 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Obavešteni članovi ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Encryption key settings" }, - "kdfAlgorithm": { - "message": "KDF algorithm" - }, "kdfIterations": { "message": "KDF iterations" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Higher KDF iterations, memory, and parallelism can help protect your master password from being brute forced by an attacker." }, - "changeKdf": { - "message": "Change KDF" - }, "encKeySettingsChanged": { "message": "Encryption key settings saved" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Off" }, - "higherKDFIterations": { - "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." - }, - "incrementsOf100,000": { - "message": "increments of 100,000" - }, - "smallIncrements": { - "message": "small increments" - }, "kdfIterationRecommends": { "message": "We recommend 600,000 or more" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "For older devices, setting your KDF too high may lead to performance issues. Increase the value in $VALUE$ and test your devices.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/sr_CY/messages.json b/apps/web/src/locales/sr_CY/messages.json index 6f0c0678ba1..fe028e4b06b 100644 --- a/apps/web/src/locales/sr_CY/messages.json +++ b/apps/web/src/locales/sr_CY/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Обавештени чланови ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Подешавања кључа шифровања" }, - "kdfAlgorithm": { - "message": "KDF Алгоритам" - }, "kdfIterations": { "message": "KDF понављања" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Веће KDF итерације, меморија и паралелизам могу помоћи у заштити ваше главне лозинке од грубе присиле од стране нападача." }, - "changeKdf": { - "message": "Променити KDF" - }, "encKeySettingsChanged": { "message": "Подешавања кључа шифровања промењена" }, @@ -5710,6 +5713,65 @@ "message": "Сазнајте више о ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Лично власништво" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Не" }, - "higherKDFIterations": { - "message": "Веће KDF итерације може помоћи у заштити ваше главне лозинке од грубе присиле од стране нападача." - }, - "incrementsOf100,000": { - "message": "повећање од 100.000" - }, - "smallIncrements": { - "message": "малим корацима" - }, "kdfIterationRecommends": { "message": "Препоручујемо 600.000 или више" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "За старије уређаје, постављање вашег КДФ-а превисоко може довести до проблема са перформансама. Повећајте вредност у $VALUE$ и тестирајте своје уређаје.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Контактирајте корисничку подршку да бисте обновили претплату." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Домен захтеван" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Име организације не може прећи 50 знакова." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index f94aa1dfc66..c9a6131ae10 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ nya lösenord i riskzonen", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Meddelade medlemmar ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Inställningar för krypteringsnyckel" }, - "kdfAlgorithm": { - "message": "KDF-algoritm" - }, "kdfIterations": { "message": "KDF-iterationer" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Högre KDF-iterationer, minne och parallellism kan hjälpa till att skydda ditt huvudlösenord från att bli brutalt tvingad av en angripare." }, - "changeKdf": { - "message": "Ändra KDF" - }, "encKeySettingsChanged": { "message": "Inställningarna för krypteringsnyckel ändrades" }, @@ -5710,6 +5713,65 @@ "message": "Läs mer om ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Tillgänglig nu" + }, + "autoConfirm": { + "message": "Automatisk bekräftelse av nya användare" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Öppna ditt Bitwarden-tillägg." + }, + "autoConfirmStep2a": { + "message": "Välj", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potentiell säkerhetsrisk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Läs mer om riskerna", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "Jag accepterar dessa risker och policyuppdateringar" + }, "personalOwnership": { "message": "Radera individuellt valv" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Av" }, - "higherKDFIterations": { - "message": "Högre KDF-iterationer kan hjälpa till att skydda ditt huvudlösenord från att bli brute forced av en angripare." - }, - "incrementsOf100,000": { - "message": "steg om 100.000" - }, - "smallIncrements": { - "message": "små steg" - }, "kdfIterationRecommends": { "message": "Vi rekommenderar 600.000 eller mer" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "För äldre enheter kan det leda till prestandaproblem om du ställer in KDF för högt. Öka värdet i $VALUE$ och testa dina enheter.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Kontakta kundtjänst för att återupprätta din prenumeration." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domänanspråk" }, + "itemAddedToFavorites": { + "message": "Objekt tillagt i favoriter" + }, + "itemRemovedFromFavorites": { + "message": "Objekt borttaget från favoriter" + }, + "copyNote": { + "message": "Kopiera anteckning" + }, "organizationNameMaxLength": { "message": "Organisationsnamnet får inte överstiga 50 tecken." }, @@ -11295,7 +11348,7 @@ "description": "Verb" }, "unArchive": { - "message": "Unarchive" + "message": "Avarkivera" }, "itemsInArchive": { "message": "Objekt i arkivet" @@ -11310,10 +11363,10 @@ "message": "Objektet skickades till arkivet" }, "itemsWereSentToArchive": { - "message": "Items were sent to archive" + "message": "Objekten har skickats till arkivet" }, "itemUnarchived": { - "message": "Item was unarchived" + "message": "Objektet har avarkiverats" }, "bulkArchiveItems": { "message": "Objekt arkiverade" @@ -11675,7 +11728,7 @@ "message": "Verifiera nu." }, "additionalStorageGB": { - "message": "Additional storage GB" + "message": "Ytterligare lagringsplats (GB)" }, "additionalServiceAccountsV2": { "message": "Ytterligare maskinkonton" @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "Visa prismodeller" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/ta/messages.json b/apps/web/src/locales/ta/messages.json index a42967cb0e2..a8fe0c062b4 100644 --- a/apps/web/src/locales/ta/messages.json +++ b/apps/web/src/locales/ta/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "அறிவிக்கப்பட்ட உறுப்பினர்கள் ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "என்க்ரிப்ஷன் சாவி அமைப்புகள்" }, - "kdfAlgorithm": { - "message": "KDF அல்காரிதம்" - }, "kdfIterations": { "message": "KDF மறுநிகழ்வுகள்" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "அதிக KDF இட்டரேஷன்கள், நினைவகம் மற்றும் பாரலலிசம் உங்கள் முதன்மை கடவுச்சொல்லை தாக்குபவர் ப்ரூட் ஃபோர்ஸ் செய்வதிலிருந்து பாதுகாக்க உதவும்." }, - "changeKdf": { - "message": "KDF-ஐ மாற்றவும்" - }, "encKeySettingsChanged": { "message": "என்கிரிப்ஷன் கீ அமைப்புகள் சேமிக்கப்பட்டன" }, @@ -5710,6 +5713,65 @@ "message": "பற்றி மேலும் அறிக ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "தனிப்பட்ட வால்ட்டை அகற்று" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "முடக்கப்பட்டுள்ளது" }, - "higherKDFIterations": { - "message": "அதிக KDF இட்டரேஷன்கள், தாக்குபவரால் உங்கள் முதன்மை கடவுச்சொல் வலுக்கட்டாயமாகப் பயன்படுத்தப்படுவதிலிருந்து பாதுகாக்க உதவும்." - }, - "incrementsOf100,000": { - "message": "100,000 ஆக அதிகரிப்பது" - }, - "smallIncrements": { - "message": "சிறிய அதிகரிப்புகள்" - }, "kdfIterationRecommends": { "message": "600,000 அல்லது அதற்கு மேல் இருக்க நாங்கள் பரிந்துரைக்கிறோம்" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "பழைய சாதனங்களுக்கு, உங்கள் KDF-ஐ மிக அதிகமாக அமைப்பது, செயல்திறன் சிக்கல்களுக்கு வழிவகுக்கலாம். $VALUE$-இல் மதிப்பை அதிகரித்து, உங்கள் சாதனங்களைச் சோதிக்கவும்.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " உங்கள் சந்தாவை மீண்டும் செயல்படுத்த, வாடிக்கையாளர் ஆதரவைத் தொடர்பு கொள்ளவும்." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "டொமைன் கோரப்பட்டது" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "அமைப்பின் பெயர் 50 எழுத்துகளைத் தாண்டக்கூடாது." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index af314c36f83..fc5efa82d50 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Encryption key settings" }, - "kdfAlgorithm": { - "message": "KDF algorithm" - }, "kdfIterations": { "message": "KDF iterations" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Higher KDF iterations, memory, and parallelism can help protect your master password from being brute forced by an attacker." }, - "changeKdf": { - "message": "Change KDF" - }, "encKeySettingsChanged": { "message": "Encryption key settings saved" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Off" }, - "higherKDFIterations": { - "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." - }, - "incrementsOf100,000": { - "message": "increments of 100,000" - }, - "smallIncrements": { - "message": "small increments" - }, "kdfIterationRecommends": { "message": "We recommend 600,000 or more" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "For older devices, setting your KDF too high may lead to performance issues. Increase the value in $VALUE$ and test your devices.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index 4d82bbd57e6..2b61c420c78 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Encryption key settings" }, - "kdfAlgorithm": { - "message": "KDF algorithm" - }, "kdfIterations": { "message": "KDF iterations" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Higher KDF iterations, memory, and parallelism can help protect your master password from being brute forced by an attacker." }, - "changeKdf": { - "message": "Change KDF" - }, "encKeySettingsChanged": { "message": "Encryption key settings saved" }, @@ -5710,6 +5713,65 @@ "message": "Learn more about the ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Off" }, - "higherKDFIterations": { - "message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker." - }, - "incrementsOf100,000": { - "message": "increments of 100,000" - }, - "smallIncrements": { - "message": "small increments" - }, "kdfIterationRecommends": { "message": "We recommend 600,000 or more" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "For older devices, setting your KDF too high may lead to performance issues. Increase the value in $VALUE$ and test your devices.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Contact Customer Support to reinstate your subscription." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Domain claimed" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index ca3ba2339b3..b9f55c329f9 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -63,7 +63,7 @@ "message": "Yeni hesap kaydı oluştur" }, "percentageCompleted": { - "message": "$PERCENT$% complete", + "message": "%$PERCENT$ tamamlandı", "placeholders": { "percent": { "content": "$1", @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ yeni parola risk altında", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Bildirilen üyeler ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Şifreleme anahtarı ayarları" }, - "kdfAlgorithm": { - "message": "KDF algoritması" - }, "kdfIterations": { "message": "KDF iterasyonu" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "KDF iterasyonu, bellek ve paralelliğin yüksek olması; ana parolanızın kaba kuvvet saldırılarından korunmasına yardımcı olabilir." }, - "changeKdf": { - "message": "KDF'i değiştir" - }, "encKeySettingsChanged": { "message": "Şifreleme anahtarı ayarları kaydedildi" }, @@ -5710,6 +5713,65 @@ "message": "Hakkında daha fazla bilgi edinin ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Kişisel kasayı kaldır" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Kapalı" }, - "higherKDFIterations": { - "message": "KDF iterasyonunun daha yüksek olması ana parolanızı kaba kuvvet saldırılarına karşı daha iyi korur." - }, - "incrementsOf100,000": { - "message": "100.000'lik artışlar" - }, - "smallIncrements": { - "message": "küçük artışlar" - }, "kdfIterationRecommends": { "message": "600.000 veya üstünü öneriyoruz" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "KDF'in çok yüksek olması eski cihazlarda performans sorunlarına yol açabilir. Değeri $VALUE$ halinde artırıp cihazlarınızı test edebilirsiniz.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Aboneliğinizi yeniden başlatmak için müşteri hizmetleri ile iletişime geçebilirsiniz." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Alan adı alındı" }, + "itemAddedToFavorites": { + "message": "Kayıt favorilere eklendi" + }, + "itemRemovedFromFavorites": { + "message": "Kayıt favorilerden silindi" + }, + "copyNote": { + "message": "Notu kopyala" + }, "organizationNameMaxLength": { "message": "Kuruluş adı 50 karakteri geçemez." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Şifreleme ayarlarını güncelle" + }, + "updateYourEncryptionSettings": { + "message": "Şifreleme ayarlarınızı güncelleyin" + }, + "updateSettings": { + "message": "Ayarları güncelle" + }, + "algorithm": { + "message": "Algoritma" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index de943a8d564..f87be8e87da 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Сповіщення учасників ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Налаштування ключа шифрування" }, - "kdfAlgorithm": { - "message": "Алгоритм KDF" - }, "kdfIterations": { "message": "Ітерації KDF" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Вищі значення KDF-ітерацій, пам'яті й паралелізму можуть допомогти захистити ваш головний пароль від грубого зламу зловмисником." }, - "changeKdf": { - "message": "Змінити KDF" - }, "encKeySettingsChanged": { "message": "Налаштування ключа шифрування збережено" }, @@ -5710,6 +5713,65 @@ "message": "Докладніше про ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Вилучити особисте сховище" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Вимк" }, - "higherKDFIterations": { - "message": "Вищі значення ітерацій KDF можуть допомогти захистити ваш головний пароль від грубого зламу зловмисником." - }, - "incrementsOf100,000": { - "message": "кроком 100 000" - }, - "smallIncrements": { - "message": "невеликі кроки" - }, "kdfIterationRecommends": { "message": "Ми рекомендуємо 600 000 або більше" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "Для старіших пристроїв занадто високе значення KDF може призвести до проблем зі швидкодією. Збільшуйте значення з $VALUE$ і тестуйте роботу на пристрої.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Зверніться до служби підтримки для відновлення передплати." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Домен заявлено" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Назва організації не може перевищувати 50 символів." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index c22cf017f18..c6ce44709ea 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Các thành viên đã được thông báo ($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "Cài đặt mã khóa" }, - "kdfAlgorithm": { - "message": "Thuật toán KDF" - }, "kdfIterations": { "message": "Số lần KDF" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "Số lần lặp KDF cao hơn, bộ nhớ và khả năng song song có thể giúp bảo vệ mật khẩu chính của bạn khỏi bị tấn công bằng phương pháp dò tìm (brute force) từ phía kẻ tấn công." }, - "changeKdf": { - "message": "Thay đổi KDF" - }, "encKeySettingsChanged": { "message": "Đã thay đổi cài đặt mã khóa" }, @@ -5710,6 +5713,65 @@ "message": "Tìm hiểu thêm về ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "Xóa kho lưu trữ riêng lẻ" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "Tắt" }, - "higherKDFIterations": { - "message": "Số vòng lặp KDF cao hơn có thể giúp bảo vệ mật khẩu chính của bạn khỏi các vụ tấn công brute force." - }, - "incrementsOf100,000": { - "message": "tăng theo bội số 100.000" - }, - "smallIncrements": { - "message": "bước nhỏ" - }, "kdfIterationRecommends": { "message": "Chúng tôi khuyến nghị 600.000 hoặc cao hơn" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "Đối với các thiết bị cũ, đặt KDF quá cao có thể gây ra vấn đề về hiệu suất. Tăng giá trị theo $VALUE$ và kiểm tra thiết bị của bạn.", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " Liên hệ với Bộ phận Hỗ trợ Khách hàng để khôi phục gói đăng ký của bạn." }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "Đã xác nhận tên miền" }, + "itemAddedToFavorites": { + "message": "Item added to favorites" + }, + "itemRemovedFromFavorites": { + "message": "Item removed from favorites" + }, + "copyNote": { + "message": "Copy note" + }, "organizationNameMaxLength": { "message": "Tên tổ chức không được vượt quá 50 ký tự." }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "View business plans" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index c4e94246fb2..aec6d62d9ba 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ 个新密码存在风险", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "已通知的成员 ($COUNT$)", "placeholders": { @@ -366,7 +375,7 @@ "message": "私密备注" }, "note": { - "message": "笔记" + "message": "备注" }, "customFields": { "message": "自定义字段" @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "加密密钥设置" }, - "kdfAlgorithm": { - "message": "KDF 算法" - }, "kdfIterations": { "message": "KDF 迭代" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "更高的 KDF 迭代、内存占用和并行可以帮助保护您的主密码免遭攻击者的暴力破解。" }, - "changeKdf": { - "message": "更改 KDF" - }, "encKeySettingsChanged": { "message": "加密密钥设置已保存" }, @@ -5710,6 +5713,65 @@ "message": "进一步了解", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "Available now" + }, + "autoConfirm": { + "message": "Automatic confirmation of new users" + }, + "autoConfirmDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked.", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "How to turn on automatic user confirmation" + }, + "autoConfirmStep1": { + "message": "Open your Bitwarden extension." + }, + "autoConfirmStep2a": { + "message": "Select", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " Turn on.", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "Successfully opened the Bitwarden browser extension. You can now activate the automatic user confirmation setting." + }, + "autoConfirmPolicyEditDescription": { + "message": "New users invited to the organization will be automatically confirmed when an admin’s device is unlocked. Before turning on this policy, please review and agree to the following: ", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "Potential security risk. " + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "Automatic user confirmation could pose a security risk to your organization’s data." + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "Learn about the risks", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "Single organization policy required. " + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "Anyone part of more than one organization will have their access revoked until they leave the other organizations." + }, + "autoConfirmSingleOrgExemption": { + "message": "Single organization policy will extend to all roles. " + }, + "autoConfirmNoEmergencyAccess": { + "message": "No emergency access. " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "Emergency Access will be removed." + }, + "autoConfirmCheckBoxLabel": { + "message": "I accept these risks and policy updates" + }, "personalOwnership": { "message": "禁用个人密码库" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "关闭" }, - "higherKDFIterations": { - "message": "更高的 KDF 迭代可以帮助保护您的主密码免遭攻击者的暴力破解。" - }, - "incrementsOf100,000": { - "message": "100,000 增量" - }, - "smallIncrements": { - "message": "小增量" - }, "kdfIterationRecommends": { "message": "我们推荐 600,000 或更高" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "对于老旧设备,将 KDF 设置得太高可能导致性能问题。以 $VALUE$ 值增加然后测试您的设备。", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": " 联系客户支持恢复您的订阅。" }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "域名已声明" }, + "itemAddedToFavorites": { + "message": "项目已添加到收藏夹" + }, + "itemRemovedFromFavorites": { + "message": "项目已移出收藏夹" + }, + "copyNote": { + "message": "复制备注" + }, "organizationNameMaxLength": { "message": "组织名称不能超过 50 个字符。" }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "查看商业方案" + }, + "updateEncryptionSettings": { + "message": "Update encryption settings" + }, + "updateYourEncryptionSettings": { + "message": "Update your encryption settings" + }, + "updateSettings": { + "message": "Update settings" + }, + "algorithm": { + "message": "Algorithm" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "Choose how Bitwarden should encrypt your vault data. All options are secure, but stronger methods offer better protection - especially against brute-force attacks. Bitwarden recommends the default setting for most users." + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "Increasing the values above the default will improve security, but your vault may take longer to unlock as a result." + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "About encryption algorithms" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 is a well-tested encryption method that balances security and performance. Good for all users." + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." } } diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index c5348401a24..c52c897e0e8 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ 組新密碼存在風險", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "已被通知的成員($COUNT$)", "placeholders": { @@ -2080,9 +2089,6 @@ "encKeySettings": { "message": "加密金鑰設定" }, - "kdfAlgorithm": { - "message": "KDF 演算法" - }, "kdfIterations": { "message": "KDF 疊代" }, @@ -2117,9 +2123,6 @@ "argon2Desc": { "message": "更高的 KDF 疊代次數、記憶體占用與平行數量可以避免您的主密碼遭到攻擊者的暴力破解。" }, - "changeKdf": { - "message": "變更 KDF" - }, "encKeySettingsChanged": { "message": "加密金鑰設定已儲存" }, @@ -5710,6 +5713,65 @@ "message": "了解更多 ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" }, + "availableNow": { + "message": "立即可用" + }, + "autoConfirm": { + "message": "新使用者自動確認" + }, + "autoConfirmDescription": { + "message": "當管理員的裝置已解鎖時,邀請至該組織的新使用者會自動完成確認。", + "description": "This is the description of the policy as it appears in the 'Policies' page" + }, + "howToTurnOnAutoConfirm": { + "message": "如何開啟自動使用者確認" + }, + "autoConfirmStep1": { + "message": "開啟你的 Bitwarden 瀏覽器擴充套件。" + }, + "autoConfirmStep2a": { + "message": "選擇", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmStep2b": { + "message": " 開啟。", + "description": "This is a fragment of a larger sencence. The whole sentence will read: 'Select Turn on.'" + }, + "autoConfirmExtensionOpened": { + "message": "已成功開啟 Bitwarden 瀏覽器擴充套件。您現在可以啟用自動使用者確認設定。" + }, + "autoConfirmPolicyEditDescription": { + "message": "在管理員的裝置解鎖時,邀請到組織的新使用者將自動獲得確認。開啟此原則之前,請先閱讀並同意以下內容:", + "description": "This is the description of the policy as it appears inside the policy edit dialog" + }, + "autoConfirmAcceptSecurityRiskTitle": { + "message": "可能的安全風險。" + }, + "autoConfirmAcceptSecurityRiskDescription": { + "message": "自動使用者確認可能對你組織的資料造成安全風險。" + }, + "autoConfirmAcceptSecurityRiskLearnMore": { + "message": "了解風險", + "description": "The is the link copy for the first check box option in the edit policy dialog" + }, + "autoConfirmSingleOrgRequired": { + "message": "需要啟用單一組織原則。" + }, + "autoConfirmSingleOrgRequiredDescription": { + "message": "任何同時屬於多個組織的成員,其存取權都會被撤銷,直到他們離開其他組織為止。" + }, + "autoConfirmSingleOrgExemption": { + "message": "單一組織原則將套用到所有角色。" + }, + "autoConfirmNoEmergencyAccess": { + "message": "無緊急存取。 " + }, + "autoConfirmNoEmergencyAccessDescription": { + "message": "將移除緊急存取。" + }, + "autoConfirmCheckBoxLabel": { + "message": "我接受這些風險與原則更新" + }, "personalOwnership": { "message": "停用個人密碼庫" }, @@ -10361,27 +10423,9 @@ "memberAccessReportAuthenticationEnabledFalse": { "message": "關閉" }, - "higherKDFIterations": { - "message": "更高的 KDF 疊代次數、記憶體占用與平行數量可以避免您的主密碼遭到攻擊者的暴力破解。" - }, - "incrementsOf100,000": { - "message": "以 100,000 為增量" - }, - "smallIncrements": { - "message": "小幅增量" - }, "kdfIterationRecommends": { "message": "我們建議使用 600,000 或以上" }, - "kdfToHighWarningIncreaseInIncrements": { - "message": "對於較舊的裝置,將 KDF 設定過高可能導致效能問題。請在 $VALUE$ 中增加數值並測試您的裝置。", - "placeholders": { - "value": { - "content": "$1", - "example": "increments of 100,000" - } - } - }, "providerReinstate": { "message": "請聯絡客服以恢復您的訂閱。" }, @@ -11024,6 +11068,15 @@ "domainClaimed": { "message": "已宣告網域" }, + "itemAddedToFavorites": { + "message": "項目已加入到最愛" + }, + "itemRemovedFromFavorites": { + "message": "項目已從最愛中移除" + }, + "copyNote": { + "message": "複製備註" + }, "organizationNameMaxLength": { "message": "組織名稱不得超過 50 個字元。" }, @@ -11856,5 +11909,32 @@ }, "viewbusinessplans": { "message": "檢視商業方案" + }, + "updateEncryptionSettings": { + "message": "更新加密設定" + }, + "updateYourEncryptionSettings": { + "message": "更新您的加密設定" + }, + "updateSettings": { + "message": "更新設定" + }, + "algorithm": { + "message": "演算法" + }, + "encryptionKeySettingsHowShouldWeEncryptYourData": { + "message": "選擇 Bitwarden 應如何加密你的密碼庫資料。所有選項都很安全,但更強的方法能提供更好的防護——特別是對抗暴力破解攻擊。對多數使用者,Bitwarden 建議使用預設設定。" + }, + "encryptionKeySettingsIncreaseImproveSecurity": { + "message": "將上述數值提高到預設以上可以增進安全性,但也可能導致你的密碼庫解鎖時間變長。" + }, + "encryptionKeySettingsAlgorithmPopoverTitle": { + "message": "關於加密演算法" + }, + "encryptionKeySettingsAlgorithmPopoverPBKDF2": { + "message": "PBKDF2-SHA256 是經過充分驗證的加密方法,在安全性與效能之間取得平衡,適用於所有使用者。" + }, + "encryptionKeySettingsAlgorithmPopoverArgon2Id": { + "message": "Argon2id 對抗現代攻擊提供更強的防護,最適合具有高效能裝置的進階使用者。" } } From 7313901a4977378ff29fd12096102d74563dbcf8 Mon Sep 17 00:00:00 2001 From: Stephon Brown Date: Fri, 24 Oct 2025 08:48:42 -0400 Subject: [PATCH 27/73] [PM-26019] Pre-Launch Payment Dialog (#16859) --- .../premium/premium-vnext.component.ts | 31 ++++++++++++++++++- .../upgrade-payment.component.ts | 11 ++++--- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/apps/web/src/app/billing/individual/premium/premium-vnext.component.ts b/apps/web/src/app/billing/individual/premium/premium-vnext.component.ts index 32c8061b10b..d25e035d1be 100644 --- a/apps/web/src/app/billing/individual/premium/premium-vnext.component.ts +++ b/apps/web/src/app/billing/individual/premium/premium-vnext.component.ts @@ -42,6 +42,13 @@ import { UnifiedUpgradeDialogStep, } from "../upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component"; +const RouteParams = { + callToAction: "callToAction", +} as const; +const RouteParamValues = { + upgradeToPremium: "upgradeToPremium", +} as const; + // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ @@ -61,6 +68,7 @@ export class PremiumVNextComponent { protected hasPremiumFromAnyOrganization$: Observable; protected hasPremiumPersonally$: Observable; protected shouldShowNewDesign$: Observable; + protected shouldShowUpgradeDialogOnInit$: Observable; protected personalPricingTiers$: Observable; protected premiumCardData$: Observable<{ tier: PersonalSubscriptionPricingTier | undefined; @@ -72,7 +80,6 @@ export class PremiumVNextComponent { price: number; features: string[]; }>; - protected subscriber!: BitwardenSubscriber; protected isSelfHost = false; private destroyRef = inject(DestroyRef); @@ -134,6 +141,17 @@ export class PremiumVNextComponent { ) .subscribe(); + this.shouldShowUpgradeDialogOnInit$ = combineLatest([ + this.hasPremiumFromAnyOrganization$, + this.hasPremiumPersonally$, + this.activatedRoute.queryParams, + ]).pipe( + map(([hasOrgPremium, hasPersonalPremium, queryParams]) => { + const cta = queryParams[RouteParams.callToAction]; + return !hasOrgPremium && !hasPersonalPremium && cta === RouteParamValues.upgradeToPremium; + }), + ); + this.personalPricingTiers$ = this.subscriptionPricingService.getPersonalSubscriptionPricingTiers$(); @@ -166,6 +184,17 @@ export class PremiumVNextComponent { }), shareReplay({ bufferSize: 1, refCount: true }), ); + + this.shouldShowUpgradeDialogOnInit$ + .pipe( + switchMap(async (shouldShowUpgradeDialogOnInit) => { + if (shouldShowUpgradeDialogOnInit) { + from(this.openUpgradeDialog("Premium")); + } + }), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe(); } private navigateToSubscriptionPage = (): Promise => diff --git a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.ts b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.ts index 5ad465455f2..f168672f23f 100644 --- a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.ts +++ b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.ts @@ -1,5 +1,5 @@ import { - AfterViewInit, + AfterViewChecked, Component, DestroyRef, input, @@ -96,7 +96,7 @@ export type UpgradePaymentParams = { providers: [UpgradePaymentService], templateUrl: "./upgrade-payment.component.html", }) -export class UpgradePaymentComponent implements OnInit, AfterViewInit { +export class UpgradePaymentComponent implements OnInit, AfterViewChecked { protected readonly selectedPlanId = input.required(); protected readonly account = input.required(); protected goBack = output(); @@ -118,6 +118,7 @@ export class UpgradePaymentComponent implements OnInit, AfterViewInit { }); protected readonly loading = signal(true); + private cartSummaryConfigured = false; private pricingTiers$!: Observable; // Cart Summary data @@ -201,9 +202,11 @@ export class UpgradePaymentComponent implements OnInit, AfterViewInit { this.loading.set(false); } - ngAfterViewInit(): void { - if (this.cartSummaryComponent) { + ngAfterViewChecked(): void { + // Configure cart summary only once when it becomes available + if (this.cartSummaryComponent && !this.cartSummaryConfigured) { this.cartSummaryComponent.isExpanded.set(false); + this.cartSummaryConfigured = true; } } From c94f93d0c6fefdf10f8f417ecd99c7e7c7df18fb Mon Sep 17 00:00:00 2001 From: neuronull <9162534+neuronull@users.noreply.github.com> Date: Fri, 24 Oct 2025 06:35:55 -0700 Subject: [PATCH 28/73] Desktop Native enable cargo deny CI check (#16935) * Desktop Native enable cargo deny CI check * make cargo-deny available * order * separate step --- .github/workflows/lint.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ae4f4f95aa6..bc78462fdb5 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -114,3 +114,12 @@ jobs: - name: Cargo sort working-directory: ./apps/desktop/desktop_native run: cargo sort --workspace --check + + - name: Install cargo-deny + uses: taiki-e/install-action@v2 + with: + tool: cargo-deny + + - name: Run cargo deny + working-directory: ./apps/desktop/desktop_native + run: cargo deny --log-level error --all-features check all From 3609127858f505f489743fb7da1b96364aad159e Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Fri, 24 Oct 2025 09:43:38 -0400 Subject: [PATCH 29/73] [PM-25683] Migrate Cipher model and sub-models (#16974) * Made domain classes ts-strict compliant and fixed spec files * Fixed domain base class and other test files * Added conditional utils and fixed small nits * removed comments * removd ts expect errors * Added removed counter * renamed test name * fixed tests --- .../src/platform/models/domain/domain-base.ts | 8 +- .../models/api/cipher-permissions.api.ts | 4 +- .../vault/models/domain/attachment.spec.ts | 16 +- .../src/vault/models/domain/attachment.ts | 92 ++--- .../src/vault/models/domain/card.spec.ts | 16 +- libs/common/src/vault/models/domain/card.ts | 81 ++--- .../src/vault/models/domain/cipher.spec.ts | 103 +++--- libs/common/src/vault/models/domain/cipher.ts | 340 ++++++++++-------- .../models/domain/fido2-credential.spec.ts | 52 +-- .../vault/models/domain/fido2-credential.ts | 135 +++---- .../src/vault/models/domain/field.spec.ts | 16 +- libs/common/src/vault/models/domain/field.ts | 53 ++- .../src/vault/models/domain/identity.spec.ts | 40 +-- .../src/vault/models/domain/identity.ts | 186 +++++----- .../src/vault/models/domain/login-uri.spec.ts | 14 +- .../src/vault/models/domain/login-uri.ts | 58 ++- .../src/vault/models/domain/login.spec.ts | 12 +- libs/common/src/vault/models/domain/login.ts | 124 +++---- .../src/vault/models/domain/password.spec.ts | 10 +- .../src/vault/models/domain/password.ts | 30 +- .../vault/models/domain/secure-note.spec.ts | 6 +- .../src/vault/models/domain/secure-note.ts | 21 +- .../src/vault/models/domain/ssh-key.spec.ts | 15 +- .../common/src/vault/models/domain/ssh-key.ts | 45 +-- .../models/request/cipher-partial.request.ts | 2 +- .../src/vault/services/cipher.service.ts | 3 +- libs/common/src/vault/utils/domain-utils.ts | 27 ++ .../individual-vault-export.service.spec.ts | 2 +- 28 files changed, 762 insertions(+), 749 deletions(-) create mode 100644 libs/common/src/vault/utils/domain-utils.ts diff --git a/libs/common/src/platform/models/domain/domain-base.ts b/libs/common/src/platform/models/domain/domain-base.ts index bab9f0f8ac7..a144353f5bc 100644 --- a/libs/common/src/platform/models/domain/domain-base.ts +++ b/libs/common/src/platform/models/domain/domain-base.ts @@ -14,15 +14,15 @@ export type DecryptedObject< // extracts shared keys from the domain and view types type EncryptableKeys = (keyof D & - ConditionalKeys) & - (keyof V & ConditionalKeys); + ConditionalKeys) & + (keyof V & ConditionalKeys); type DomainEncryptableKeys = { - [key in ConditionalKeys]: EncString | null; + [key in ConditionalKeys]?: EncString | null | undefined; }; type ViewEncryptableKeys = { - [key in ConditionalKeys]: string | null; + [key in ConditionalKeys]?: string | null | undefined; }; // https://contributing.bitwarden.com/architecture/clients/data-model#domain diff --git a/libs/common/src/vault/models/api/cipher-permissions.api.ts b/libs/common/src/vault/models/api/cipher-permissions.api.ts index f9b62c4fc8d..cca5ffce79e 100644 --- a/libs/common/src/vault/models/api/cipher-permissions.api.ts +++ b/libs/common/src/vault/models/api/cipher-permissions.api.ts @@ -24,7 +24,9 @@ export class CipherPermissionsApi extends BaseResponse implements SdkCipherPermi /** * Converts the SDK CipherPermissionsApi to a CipherPermissionsApi. */ - static fromSdkCipherPermissions(obj: SdkCipherPermissions): CipherPermissionsApi | undefined { + static fromSdkCipherPermissions( + obj: SdkCipherPermissions | undefined, + ): CipherPermissionsApi | undefined { if (!obj) { return undefined; } diff --git a/libs/common/src/vault/models/domain/attachment.spec.ts b/libs/common/src/vault/models/domain/attachment.spec.ts index 93f693f14c0..972c77537ff 100644 --- a/libs/common/src/vault/models/domain/attachment.spec.ts +++ b/libs/common/src/vault/models/domain/attachment.spec.ts @@ -32,12 +32,12 @@ describe("Attachment", () => { const attachment = new Attachment(data); expect(attachment).toEqual({ - id: null, - url: null, + id: undefined, + url: undefined, size: undefined, - sizeName: null, - key: null, - fileName: null, + sizeName: undefined, + key: undefined, + fileName: undefined, }); }); @@ -79,6 +79,8 @@ describe("Attachment", () => { attachment.key = mockEnc("key"); attachment.fileName = mockEnc("fileName"); + const userKey = new SymmetricCryptoKey(makeStaticByteArray(64)); + keyService.getUserKey.mockResolvedValue(userKey as UserKey); encryptService.decryptFileData.mockResolvedValue(makeStaticByteArray(32)); encryptService.unwrapSymmetricKey.mockResolvedValue( new SymmetricCryptoKey(makeStaticByteArray(64)), @@ -152,8 +154,8 @@ describe("Attachment", () => { expect(actual).toBeInstanceOf(Attachment); }); - it("returns null if object is null", () => { - expect(Attachment.fromJSON(null)).toBeNull(); + it("returns undefined if object is null", () => { + expect(Attachment.fromJSON(null)).toBeUndefined(); }); }); diff --git a/libs/common/src/vault/models/domain/attachment.ts b/libs/common/src/vault/models/domain/attachment.ts index 4ace8ce0e77..7b43af9be55 100644 --- a/libs/common/src/vault/models/domain/attachment.ts +++ b/libs/common/src/vault/models/domain/attachment.ts @@ -1,23 +1,23 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Jsonify } from "type-fest"; +import { OrgKey, UserKey } from "@bitwarden/common/types/key"; import { Attachment as SdkAttachment } from "@bitwarden/sdk-internal"; import { EncString } from "../../../key-management/crypto/models/enc-string"; import { Utils } from "../../../platform/misc/utils"; import Domain from "../../../platform/models/domain/domain-base"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; +import { conditionalEncString, encStringFrom } from "../../utils/domain-utils"; import { AttachmentData } from "../data/attachment.data"; import { AttachmentView } from "../view/attachment.view"; export class Attachment extends Domain { - id: string; - url: string; - size: string; - sizeName: string; // Readable size, ex: "4.2 KB" or "1.43 GB" - key: EncString; - fileName: EncString; + id?: string; + url?: string; + size?: string; + sizeName?: string; // Readable size, ex: "4.2 KB" or "1.43 GB" + key?: EncString; + fileName?: EncString; constructor(obj?: AttachmentData) { super(); @@ -25,32 +25,24 @@ export class Attachment extends Domain { return; } + this.id = obj.id; + this.url = obj.url; this.size = obj.size; - this.buildDomainModel( - this, - obj, - { - id: null, - url: null, - sizeName: null, - fileName: null, - key: null, - }, - ["id", "url", "sizeName"], - ); + this.sizeName = obj.sizeName; + this.fileName = conditionalEncString(obj.fileName); + this.key = conditionalEncString(obj.key); } async decrypt( - orgId: string, + orgId: string | undefined, context = "No Cipher Context", encKey?: SymmetricCryptoKey, ): Promise { const view = await this.decryptObj( this, - // @ts-expect-error ViewEncryptableKeys type should be fixed to allow for optional values, but is out of scope for now. new AttachmentView(this), ["fileName"], - orgId, + orgId ?? null, encKey, "DomainType: Attachment; " + context, ); @@ -63,30 +55,46 @@ export class Attachment extends Domain { return view; } - private async decryptAttachmentKey(orgId: string, encKey?: SymmetricCryptoKey) { + private async decryptAttachmentKey( + orgId: string | undefined, + encKey?: SymmetricCryptoKey, + ): Promise { try { + if (this.key == null) { + return undefined; + } + if (encKey == null) { - encKey = await this.getKeyForDecryption(orgId); + const key = await this.getKeyForDecryption(orgId); + + // If we don't have a key, we can't decrypt + if (key == null) { + return undefined; + } + + encKey = key; } const encryptService = Utils.getContainerService().getEncryptService(); const decValue = await encryptService.unwrapSymmetricKey(this.key, encKey); return decValue; - // FIXME: Remove when updating file. Eslint update - // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { - // TODO: error? + // eslint-disable-next-line no-console + console.error("[Attachment] Error decrypting attachment", e); + return undefined; } } - private async getKeyForDecryption(orgId: string) { + private async getKeyForDecryption(orgId: string | undefined): Promise { const keyService = Utils.getContainerService().getKeyService(); return orgId != null ? await keyService.getOrgKey(orgId) : await keyService.getUserKey(); } toAttachmentData(): AttachmentData { const a = new AttachmentData(); - a.size = this.size; + if (this.size != null) { + a.size = this.size; + } this.buildDataModel( this, a, @@ -102,18 +110,20 @@ export class Attachment extends Domain { return a; } - static fromJSON(obj: Partial>): Attachment { + static fromJSON(obj: Partial> | undefined): Attachment | undefined { if (obj == null) { - return null; + return undefined; } - const key = EncString.fromJSON(obj.key); - const fileName = EncString.fromJSON(obj.fileName); + const attachment = new Attachment(); + attachment.id = obj.id; + attachment.url = obj.url; + attachment.size = obj.size; + attachment.sizeName = obj.sizeName; + attachment.key = encStringFrom(obj.key); + attachment.fileName = encStringFrom(obj.fileName); - return Object.assign(new Attachment(), obj, { - key, - fileName, - }); + return attachment; } /** @@ -136,7 +146,7 @@ export class Attachment extends Domain { * Maps an SDK Attachment object to an Attachment * @param obj - The SDK attachment object */ - static fromSdkAttachment(obj: SdkAttachment): Attachment | undefined { + static fromSdkAttachment(obj?: SdkAttachment): Attachment | undefined { if (!obj) { return undefined; } @@ -146,8 +156,8 @@ export class Attachment extends Domain { attachment.url = obj.url; attachment.size = obj.size; attachment.sizeName = obj.sizeName; - attachment.fileName = EncString.fromJSON(obj.fileName); - attachment.key = EncString.fromJSON(obj.key); + attachment.fileName = encStringFrom(obj.fileName); + attachment.key = encStringFrom(obj.key); return attachment; } diff --git a/libs/common/src/vault/models/domain/card.spec.ts b/libs/common/src/vault/models/domain/card.spec.ts index 4da62c631d6..a4d242329a4 100644 --- a/libs/common/src/vault/models/domain/card.spec.ts +++ b/libs/common/src/vault/models/domain/card.spec.ts @@ -22,12 +22,12 @@ describe("Card", () => { const card = new Card(data); expect(card).toEqual({ - cardholderName: null, - brand: null, - number: null, - expMonth: null, - expYear: null, - code: null, + cardholderName: undefined, + brand: undefined, + number: undefined, + expMonth: undefined, + expYear: undefined, + code: undefined, }); }); @@ -94,8 +94,8 @@ describe("Card", () => { expect(actual).toBeInstanceOf(Card); }); - it("returns null if object is null", () => { - expect(Card.fromJSON(null)).toBeNull(); + it("returns undefined if object is null", () => { + expect(Card.fromJSON(null)).toBeUndefined(); }); }); diff --git a/libs/common/src/vault/models/domain/card.ts b/libs/common/src/vault/models/domain/card.ts index 89cc361b454..b3a087d44fb 100644 --- a/libs/common/src/vault/models/domain/card.ts +++ b/libs/common/src/vault/models/domain/card.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Jsonify } from "type-fest"; import { Card as SdkCard } from "@bitwarden/sdk-internal"; @@ -7,16 +5,17 @@ import { Card as SdkCard } from "@bitwarden/sdk-internal"; import { EncString } from "../../../key-management/crypto/models/enc-string"; import Domain from "../../../platform/models/domain/domain-base"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; +import { conditionalEncString, encStringFrom } from "../../utils/domain-utils"; import { CardData } from "../data/card.data"; import { CardView } from "../view/card.view"; export class Card extends Domain { - cardholderName: EncString; - brand: EncString; - number: EncString; - expMonth: EncString; - expYear: EncString; - code: EncString; + cardholderName?: EncString; + brand?: EncString; + number?: EncString; + expMonth?: EncString; + expYear?: EncString; + code?: EncString; constructor(obj?: CardData) { super(); @@ -24,23 +23,16 @@ export class Card extends Domain { return; } - this.buildDomainModel( - this, - obj, - { - cardholderName: null, - brand: null, - number: null, - expMonth: null, - expYear: null, - code: null, - }, - [], - ); + this.cardholderName = conditionalEncString(obj.cardholderName); + this.brand = conditionalEncString(obj.brand); + this.number = conditionalEncString(obj.number); + this.expMonth = conditionalEncString(obj.expMonth); + this.expYear = conditionalEncString(obj.expYear); + this.code = conditionalEncString(obj.code); } async decrypt( - orgId: string, + orgId: string | undefined, context = "No Cipher Context", encKey?: SymmetricCryptoKey, ): Promise { @@ -48,7 +40,7 @@ export class Card extends Domain { this, new CardView(), ["cardholderName", "brand", "number", "expMonth", "expYear", "code"], - orgId, + orgId ?? null, encKey, "DomainType: Card; " + context, ); @@ -67,25 +59,20 @@ export class Card extends Domain { return c; } - static fromJSON(obj: Partial>): Card { + static fromJSON(obj: Partial> | undefined): Card | undefined { if (obj == null) { - return null; + return undefined; } - const cardholderName = EncString.fromJSON(obj.cardholderName); - const brand = EncString.fromJSON(obj.brand); - const number = EncString.fromJSON(obj.number); - const expMonth = EncString.fromJSON(obj.expMonth); - const expYear = EncString.fromJSON(obj.expYear); - const code = EncString.fromJSON(obj.code); - return Object.assign(new Card(), obj, { - cardholderName, - brand, - number, - expMonth, - expYear, - code, - }); + const card = new Card(); + card.cardholderName = encStringFrom(obj.cardholderName); + card.brand = encStringFrom(obj.brand); + card.number = encStringFrom(obj.number); + card.expMonth = encStringFrom(obj.expMonth); + card.expYear = encStringFrom(obj.expYear); + card.code = encStringFrom(obj.code); + + return card; } /** @@ -108,18 +95,18 @@ export class Card extends Domain { * Maps an SDK Card object to a Card * @param obj - The SDK Card object */ - static fromSdkCard(obj: SdkCard): Card | undefined { - if (obj == null) { + static fromSdkCard(obj?: SdkCard): Card | undefined { + if (!obj) { return undefined; } const card = new Card(); - card.cardholderName = EncString.fromJSON(obj.cardholderName); - card.brand = EncString.fromJSON(obj.brand); - card.number = EncString.fromJSON(obj.number); - card.expMonth = EncString.fromJSON(obj.expMonth); - card.expYear = EncString.fromJSON(obj.expYear); - card.code = EncString.fromJSON(obj.code); + card.cardholderName = encStringFrom(obj.cardholderName); + card.brand = encStringFrom(obj.brand); + card.number = encStringFrom(obj.number); + card.expMonth = encStringFrom(obj.expMonth); + card.expYear = encStringFrom(obj.expYear); + card.code = encStringFrom(obj.code); return card; } diff --git a/libs/common/src/vault/models/domain/cipher.spec.ts b/libs/common/src/vault/models/domain/cipher.spec.ts index c2cb99740db..4052c9e5338 100644 --- a/libs/common/src/vault/models/domain/cipher.spec.ts +++ b/libs/common/src/vault/models/domain/cipher.spec.ts @@ -44,31 +44,28 @@ describe("Cipher DTO", () => { const data = new CipherData(); const cipher = new Cipher(data); - expect(cipher).toEqual({ - initializerKey: InitializerKey.Cipher, - id: null, - organizationId: null, - folderId: null, - name: null, - notes: null, - type: undefined, - favorite: undefined, - organizationUseTotp: undefined, - edit: undefined, - viewPassword: true, - revisionDate: null, - collectionIds: undefined, - localData: null, - creationDate: null, - deletedDate: undefined, - reprompt: undefined, - attachments: null, - fields: null, - passwordHistory: null, - key: null, - permissions: undefined, - archivedDate: undefined, - }); + expect(cipher.id).toBeUndefined(); + expect(cipher.organizationId).toBeUndefined(); + expect(cipher.folderId).toBeUndefined(); + expect(cipher.name).toBeInstanceOf(EncString); + expect(cipher.notes).toBeUndefined(); + expect(cipher.type).toBeUndefined(); + expect(cipher.favorite).toBeUndefined(); + expect(cipher.organizationUseTotp).toBeUndefined(); + expect(cipher.edit).toBeUndefined(); + expect(cipher.viewPassword).toBeUndefined(); + expect(cipher.revisionDate).toBeInstanceOf(Date); + expect(cipher.collectionIds).toEqual([]); + expect(cipher.localData).toBeUndefined(); + expect(cipher.creationDate).toBeInstanceOf(Date); + expect(cipher.deletedDate).toBeUndefined(); + expect(cipher.reprompt).toBeUndefined(); + expect(cipher.attachments).toBeUndefined(); + expect(cipher.fields).toBeUndefined(); + expect(cipher.passwordHistory).toBeUndefined(); + expect(cipher.key).toBeUndefined(); + expect(cipher.permissions).toBeUndefined(); + expect(cipher.archivedDate).toBeUndefined(); }); it("Decrypt should handle cipher key error", async () => { @@ -121,7 +118,7 @@ describe("Cipher DTO", () => { edit: true, viewPassword: true, decryptionFailure: true, - collectionIds: undefined, + collectionIds: [], revisionDate: new Date("2022-01-31T12:00:00.000Z"), creationDate: new Date("2022-01-01T12:00:00.000Z"), deletedDate: undefined, @@ -155,6 +152,7 @@ describe("Cipher DTO", () => { reprompt: CipherRepromptType.None, key: "EncryptedString", archivedDate: undefined, + collectionIds: [], login: { uris: [ { @@ -223,8 +221,8 @@ describe("Cipher DTO", () => { edit: true, viewPassword: true, revisionDate: new Date("2022-01-31T12:00:00.000Z"), - collectionIds: undefined, - localData: null, + collectionIds: [], + localData: undefined, creationDate: new Date("2022-01-01T12:00:00.000Z"), deletedDate: undefined, permissions: new CipherPermissionsApi(), @@ -265,13 +263,13 @@ describe("Cipher DTO", () => { ], fields: [ { - linkedId: null, + linkedId: undefined, name: { encryptedString: "EncryptedString", encryptionType: 0 }, type: 0, value: { encryptedString: "EncryptedString", encryptionType: 0 }, }, { - linkedId: null, + linkedId: undefined, name: { encryptedString: "EncryptedString", encryptionType: 0 }, type: 1, value: { encryptedString: "EncryptedString", encryptionType: 0 }, @@ -348,7 +346,7 @@ describe("Cipher DTO", () => { attachments: [], fields: [], passwordHistory: [], - collectionIds: undefined, + collectionIds: [], revisionDate: new Date("2022-01-31T12:00:00.000Z"), creationDate: new Date("2022-01-01T12:00:00.000Z"), deletedDate: undefined, @@ -380,6 +378,7 @@ describe("Cipher DTO", () => { deletedDate: undefined, reprompt: CipherRepromptType.None, key: "EncKey", + collectionIds: [], secureNote: { type: SecureNoteType.Generic, }, @@ -404,15 +403,15 @@ describe("Cipher DTO", () => { edit: true, viewPassword: true, revisionDate: new Date("2022-01-31T12:00:00.000Z"), - collectionIds: undefined, - localData: null, + collectionIds: [], + localData: undefined, creationDate: new Date("2022-01-01T12:00:00.000Z"), deletedDate: undefined, reprompt: 0, secureNote: { type: SecureNoteType.Generic }, - attachments: null, - fields: null, - passwordHistory: null, + attachments: undefined, + fields: undefined, + passwordHistory: undefined, key: { encryptedString: "EncKey", encryptionType: 0 }, permissions: new CipherPermissionsApi(), archivedDate: undefined, @@ -475,7 +474,7 @@ describe("Cipher DTO", () => { attachments: [], fields: [], passwordHistory: [], - collectionIds: undefined, + collectionIds: [], revisionDate: new Date("2022-01-31T12:00:00.000Z"), creationDate: new Date("2022-01-01T12:00:00.000Z"), deletedDate: undefined, @@ -507,6 +506,7 @@ describe("Cipher DTO", () => { deletedDate: undefined, permissions: new CipherPermissionsApi(), reprompt: CipherRepromptType.None, + collectionIds: [], card: { cardholderName: "EncryptedString", brand: "EncryptedString", @@ -536,8 +536,8 @@ describe("Cipher DTO", () => { edit: true, viewPassword: true, revisionDate: new Date("2022-01-31T12:00:00.000Z"), - collectionIds: undefined, - localData: null, + collectionIds: [], + localData: undefined, creationDate: new Date("2022-01-01T12:00:00.000Z"), deletedDate: undefined, reprompt: 0, @@ -549,9 +549,9 @@ describe("Cipher DTO", () => { expYear: { encryptedString: "EncryptedString", encryptionType: 0 }, code: { encryptedString: "EncryptedString", encryptionType: 0 }, }, - attachments: null, - fields: null, - passwordHistory: null, + attachments: undefined, + fields: undefined, + passwordHistory: undefined, key: { encryptedString: "EncKey", encryptionType: 0 }, permissions: new CipherPermissionsApi(), archivedDate: undefined, @@ -620,7 +620,7 @@ describe("Cipher DTO", () => { attachments: [], fields: [], passwordHistory: [], - collectionIds: undefined, + collectionIds: [], revisionDate: new Date("2022-01-31T12:00:00.000Z"), creationDate: new Date("2022-01-01T12:00:00.000Z"), deletedDate: undefined, @@ -654,6 +654,7 @@ describe("Cipher DTO", () => { reprompt: CipherRepromptType.None, key: "EncKey", archivedDate: undefined, + collectionIds: [], identity: { title: "EncryptedString", firstName: "EncryptedString", @@ -693,8 +694,8 @@ describe("Cipher DTO", () => { edit: true, viewPassword: true, revisionDate: new Date("2022-01-31T12:00:00.000Z"), - collectionIds: undefined, - localData: null, + collectionIds: [], + localData: undefined, creationDate: new Date("2022-01-01T12:00:00.000Z"), deletedDate: undefined, reprompt: 0, @@ -719,9 +720,9 @@ describe("Cipher DTO", () => { passportNumber: { encryptedString: "EncryptedString", encryptionType: 0 }, licenseNumber: { encryptedString: "EncryptedString", encryptionType: 0 }, }, - attachments: null, - fields: null, - passwordHistory: null, + attachments: undefined, + fields: undefined, + passwordHistory: undefined, key: { encryptedString: "EncKey", encryptionType: 0 }, permissions: new CipherPermissionsApi(), }); @@ -789,7 +790,7 @@ describe("Cipher DTO", () => { attachments: [], fields: [], passwordHistory: [], - collectionIds: undefined, + collectionIds: [], revisionDate: new Date("2022-01-31T12:00:00.000Z"), creationDate: new Date("2022-01-01T12:00:00.000Z"), deletedDate: undefined, @@ -858,8 +859,8 @@ describe("Cipher DTO", () => { expect(actual).toMatchObject(expected); }); - it("returns null if object is null", () => { - expect(Cipher.fromJSON(null)).toBeNull(); + it("returns undefined if object is undefined", () => { + expect(Cipher.fromJSON(undefined)).toBeUndefined(); }); }); diff --git a/libs/common/src/vault/models/domain/cipher.ts b/libs/common/src/vault/models/domain/cipher.ts index 8ba81c7bbd3..5e284232936 100644 --- a/libs/common/src/vault/models/domain/cipher.ts +++ b/libs/common/src/vault/models/domain/cipher.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Jsonify } from "type-fest"; import { Cipher as SdkCipher } from "@bitwarden/sdk-internal"; @@ -13,6 +11,7 @@ import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-cr import { InitializerKey } from "../../../platform/services/cryptography/initializer-key"; import { CipherRepromptType } from "../../enums/cipher-reprompt-type"; import { CipherType } from "../../enums/cipher-type"; +import { conditionalEncString, encStringFrom } from "../../utils/domain-utils"; import { CipherPermissionsApi } from "../api/cipher-permissions.api"; import { CipherData } from "../data/cipher.data"; import { LocalData, fromSdkLocalData, toSdkLocalData } from "../data/local.data"; @@ -33,71 +32,60 @@ import { SshKey } from "./ssh-key"; export class Cipher extends Domain implements Decryptable { readonly initializerKey = InitializerKey.Cipher; - id: string; - organizationId: string; - folderId: string; - name: EncString; - notes: EncString; - type: CipherType; - favorite: boolean; - organizationUseTotp: boolean; - edit: boolean; - viewPassword: boolean; - permissions: CipherPermissionsApi; + id: string = ""; + organizationId?: string; + folderId?: string; + name: EncString = new EncString(""); + notes?: EncString; + type: CipherType = CipherType.Login; + favorite: boolean = false; + organizationUseTotp: boolean = false; + edit: boolean = false; + viewPassword: boolean = true; + permissions?: CipherPermissionsApi; revisionDate: Date; - localData: LocalData; - login: Login; - identity: Identity; - card: Card; - secureNote: SecureNote; - sshKey: SshKey; - attachments: Attachment[]; - fields: Field[]; - passwordHistory: Password[]; - collectionIds: string[]; + localData?: LocalData; + login?: Login; + identity?: Identity; + card?: Card; + secureNote?: SecureNote; + sshKey?: SshKey; + attachments?: Attachment[]; + fields?: Field[]; + passwordHistory?: Password[]; + collectionIds: string[] = []; creationDate: Date; - deletedDate: Date | undefined; - archivedDate: Date | undefined; - reprompt: CipherRepromptType; - key: EncString; + deletedDate?: Date; + archivedDate?: Date; + reprompt: CipherRepromptType = CipherRepromptType.None; + key?: EncString; - constructor(obj?: CipherData, localData: LocalData = null) { + constructor(obj?: CipherData, localData?: LocalData) { super(); if (obj == null) { + this.creationDate = this.revisionDate = new Date(); return; } - this.buildDomainModel( - this, - obj, - { - id: null, - organizationId: null, - folderId: null, - name: null, - notes: null, - key: null, - }, - ["id", "organizationId", "folderId"], - ); - + this.id = obj.id; + this.organizationId = obj.organizationId; + this.folderId = obj.folderId; + this.name = new EncString(obj.name); + this.notes = conditionalEncString(obj.notes); this.type = obj.type; this.favorite = obj.favorite; this.organizationUseTotp = obj.organizationUseTotp; this.edit = obj.edit; - if (obj.viewPassword != null) { - this.viewPassword = obj.viewPassword; - } else { - this.viewPassword = true; // Default for already synced Ciphers without viewPassword - } + this.viewPassword = obj.viewPassword; this.permissions = obj.permissions; - this.revisionDate = obj.revisionDate != null ? new Date(obj.revisionDate) : null; - this.collectionIds = obj.collectionIds; + this.revisionDate = new Date(obj.revisionDate); this.localData = localData; - this.creationDate = obj.creationDate != null ? new Date(obj.creationDate) : null; + this.collectionIds = obj.collectionIds ?? []; + this.creationDate = new Date(obj.creationDate); this.deletedDate = obj.deletedDate != null ? new Date(obj.deletedDate) : undefined; this.archivedDate = obj.archivedDate != null ? new Date(obj.archivedDate) : undefined; this.reprompt = obj.reprompt; + this.key = conditionalEncString(obj.key); switch (this.type) { case CipherType.Login: @@ -121,20 +109,14 @@ export class Cipher extends Domain implements Decryptable { if (obj.attachments != null) { this.attachments = obj.attachments.map((a) => new Attachment(a)); - } else { - this.attachments = null; } if (obj.fields != null) { this.fields = obj.fields.map((f) => new Field(f)); - } else { - this.fields = null; } if (obj.passwordHistory != null) { this.passwordHistory = obj.passwordHistory.map((ph) => new Password(ph)); - } else { - this.passwordHistory = null; } } @@ -161,46 +143,54 @@ export class Cipher extends Domain implements Decryptable { await this.decryptObj( this, - // @ts-expect-error Ciphers have optional Ids which are getting swallowed by the ViewEncryptableKeys type - // The ViewEncryptableKeys type should be fixed to allow for optional Ids, but is out of scope for now. model, ["name", "notes"], - this.organizationId, + this.organizationId ?? null, encKey, ); switch (this.type) { case CipherType.Login: - model.login = await this.login.decrypt( - this.organizationId, - bypassValidation, - `Cipher Id: ${this.id}`, - encKey, - ); + if (this.login != null) { + model.login = await this.login.decrypt( + this.organizationId, + bypassValidation, + `Cipher Id: ${this.id}`, + encKey, + ); + } break; case CipherType.SecureNote: - model.secureNote = await this.secureNote.decrypt( - this.organizationId, - `Cipher Id: ${this.id}`, - encKey, - ); + if (this.secureNote != null) { + model.secureNote = await this.secureNote.decrypt(); + } break; case CipherType.Card: - model.card = await this.card.decrypt(this.organizationId, `Cipher Id: ${this.id}`, encKey); + if (this.card != null) { + model.card = await this.card.decrypt( + this.organizationId, + `Cipher Id: ${this.id}`, + encKey, + ); + } break; case CipherType.Identity: - model.identity = await this.identity.decrypt( - this.organizationId, - `Cipher Id: ${this.id}`, - encKey, - ); + if (this.identity != null) { + model.identity = await this.identity.decrypt( + this.organizationId, + `Cipher Id: ${this.id}`, + encKey, + ); + } break; case CipherType.SshKey: - model.sshKey = await this.sshKey.decrypt( - this.organizationId, - `Cipher Id: ${this.id}`, - encKey, - ); + if (this.sshKey != null) { + model.sshKey = await this.sshKey.decrypt( + this.organizationId, + `Cipher Id: ${this.id}`, + encKey, + ); + } break; default: break; @@ -209,9 +199,12 @@ export class Cipher extends Domain implements Decryptable { if (this.attachments != null && this.attachments.length > 0) { const attachments: AttachmentView[] = []; for (const attachment of this.attachments) { - attachments.push( - await attachment.decrypt(this.organizationId, `Cipher Id: ${this.id}`, encKey), + const decryptedAttachment = await attachment.decrypt( + this.organizationId, + `Cipher Id: ${this.id}`, + encKey, ); + attachments.push(decryptedAttachment); } model.attachments = attachments; } @@ -219,7 +212,8 @@ export class Cipher extends Domain implements Decryptable { if (this.fields != null && this.fields.length > 0) { const fields: FieldView[] = []; for (const field of this.fields) { - fields.push(await field.decrypt(this.organizationId, encKey)); + const decryptedField = await field.decrypt(this.organizationId, encKey); + fields.push(decryptedField); } model.fields = fields; } @@ -227,7 +221,8 @@ export class Cipher extends Domain implements Decryptable { if (this.passwordHistory != null && this.passwordHistory.length > 0) { const passwordHistory: PasswordHistoryView[] = []; for (const ph of this.passwordHistory) { - passwordHistory.push(await ph.decrypt(this.organizationId, encKey)); + const decryptedPh = await ph.decrypt(this.organizationId, encKey); + passwordHistory.push(decryptedPh); } model.passwordHistory = passwordHistory; } @@ -238,20 +233,32 @@ export class Cipher extends Domain implements Decryptable { toCipherData(): CipherData { const c = new CipherData(); c.id = this.id; - c.organizationId = this.organizationId; - c.folderId = this.folderId; + if (this.organizationId != null) { + c.organizationId = this.organizationId; + } + + if (this.folderId != null) { + c.folderId = this.folderId; + } c.edit = this.edit; c.viewPassword = this.viewPassword; c.organizationUseTotp = this.organizationUseTotp; c.favorite = this.favorite; - c.revisionDate = this.revisionDate != null ? this.revisionDate.toISOString() : null; + c.revisionDate = this.revisionDate.toISOString(); c.type = this.type; c.collectionIds = this.collectionIds; - c.creationDate = this.creationDate != null ? this.creationDate.toISOString() : null; + c.creationDate = this.creationDate.toISOString(); c.deletedDate = this.deletedDate != null ? this.deletedDate.toISOString() : undefined; c.reprompt = this.reprompt; - c.key = this.key?.encryptedString; - c.permissions = this.permissions; + + if (this.key != null && this.key.encryptedString != null) { + c.key = this.key.encryptedString; + } + + if (this.permissions != null) { + c.permissions = this.permissions; + } + c.archivedDate = this.archivedDate != null ? this.archivedDate.toISOString() : undefined; this.buildDataModel(this, c, { @@ -261,19 +268,29 @@ export class Cipher extends Domain implements Decryptable { switch (c.type) { case CipherType.Login: - c.login = this.login.toLoginData(); + if (this.login != null) { + c.login = this.login.toLoginData(); + } break; case CipherType.SecureNote: - c.secureNote = this.secureNote.toSecureNoteData(); + if (this.secureNote != null) { + c.secureNote = this.secureNote.toSecureNoteData(); + } break; case CipherType.Card: - c.card = this.card.toCardData(); + if (this.card != null) { + c.card = this.card.toCardData(); + } break; case CipherType.Identity: - c.identity = this.identity.toIdentityData(); + if (this.identity != null) { + c.identity = this.identity.toIdentityData(); + } break; case CipherType.SshKey: - c.sshKey = this.sshKey.toSshKeyData(); + if (this.sshKey != null) { + c.sshKey = this.sshKey.toSshKeyData(); + } break; default: break; @@ -291,51 +308,71 @@ export class Cipher extends Domain implements Decryptable { return c; } - static fromJSON(obj: Jsonify) { + static fromJSON(obj: Jsonify | undefined): Cipher | undefined { if (obj == null) { - return null; + return undefined; } const domain = new Cipher(); - const name = EncString.fromJSON(obj.name); - const notes = EncString.fromJSON(obj.notes); - const creationDate = obj.creationDate == null ? null : new Date(obj.creationDate); - const revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate); - const deletedDate = obj.deletedDate == null ? undefined : new Date(obj.deletedDate); - const attachments = obj.attachments?.map((a: any) => Attachment.fromJSON(a)); - const fields = obj.fields?.map((f: any) => Field.fromJSON(f)); - const passwordHistory = obj.passwordHistory?.map((ph: any) => Password.fromJSON(ph)); - const key = EncString.fromJSON(obj.key); - const archivedDate = obj.archivedDate == null ? undefined : new Date(obj.archivedDate); - Object.assign(domain, obj, { - name, - notes, - creationDate, - revisionDate, - deletedDate, - attachments, - fields, - passwordHistory, - key, - archivedDate, - }); + domain.id = obj.id; + domain.organizationId = obj.organizationId; + domain.folderId = obj.folderId; + domain.type = obj.type; + domain.favorite = obj.favorite; + domain.organizationUseTotp = obj.organizationUseTotp; + domain.edit = obj.edit; + domain.viewPassword = obj.viewPassword; + + if (obj.permissions != null) { + domain.permissions = new CipherPermissionsApi(obj.permissions); + } + + domain.collectionIds = obj.collectionIds; + domain.localData = obj.localData; + domain.reprompt = obj.reprompt; + domain.creationDate = new Date(obj.creationDate); + domain.revisionDate = new Date(obj.revisionDate); + domain.deletedDate = obj.deletedDate != null ? new Date(obj.deletedDate) : undefined; + domain.archivedDate = obj.archivedDate != null ? new Date(obj.archivedDate) : undefined; + domain.name = EncString.fromJSON(obj.name); + domain.notes = encStringFrom(obj.notes); + domain.key = encStringFrom(obj.key); + domain.attachments = obj.attachments + ?.map((a: any) => Attachment.fromJSON(a)) + .filter((a): a is Attachment => a != null); + domain.fields = obj.fields + ?.map((f: any) => Field.fromJSON(f)) + .filter((f): f is Field => f != null); + domain.passwordHistory = obj.passwordHistory + ?.map((ph: any) => Password.fromJSON(ph)) + .filter((ph): ph is Password => ph != null); switch (obj.type) { case CipherType.Card: - domain.card = Card.fromJSON(obj.card); + if (obj.card != null) { + domain.card = Card.fromJSON(obj.card); + } break; case CipherType.Identity: - domain.identity = Identity.fromJSON(obj.identity); + if (obj.identity != null) { + domain.identity = Identity.fromJSON(obj.identity); + } break; case CipherType.Login: - domain.login = Login.fromJSON(obj.login); + if (obj.login != null) { + domain.login = Login.fromJSON(obj.login); + } break; case CipherType.SecureNote: - domain.secureNote = SecureNote.fromJSON(obj.secureNote); + if (obj.secureNote != null) { + domain.secureNote = SecureNote.fromJSON(obj.secureNote); + } break; case CipherType.SshKey: - domain.sshKey = SshKey.fromJSON(obj.sshKey); + if (obj.sshKey != null) { + domain.sshKey = SshKey.fromJSON(obj.sshKey); + } break; default: break; @@ -359,22 +396,22 @@ export class Cipher extends Domain implements Decryptable { name: this.name.toSdk(), notes: this.notes?.toSdk(), type: this.type, - favorite: this.favorite ?? false, - organizationUseTotp: this.organizationUseTotp ?? false, - edit: this.edit ?? true, + favorite: this.favorite, + organizationUseTotp: this.organizationUseTotp, + edit: this.edit, permissions: this.permissions ? { delete: this.permissions.delete, restore: this.permissions.restore, } : undefined, - viewPassword: this.viewPassword ?? true, + viewPassword: this.viewPassword, localData: toSdkLocalData(this.localData), attachments: this.attachments?.map((a) => a.toSdkAttachment()), fields: this.fields?.map((f) => f.toSdkField()), passwordHistory: this.passwordHistory?.map((ph) => ph.toSdkPasswordHistory()), - revisionDate: this.revisionDate?.toISOString(), - creationDate: this.creationDate?.toISOString(), + revisionDate: this.revisionDate.toISOString(), + creationDate: this.creationDate.toISOString(), deletedDate: this.deletedDate?.toISOString(), archivedDate: this.archivedDate?.toISOString(), reprompt: this.reprompt, @@ -388,19 +425,29 @@ export class Cipher extends Domain implements Decryptable { switch (this.type) { case CipherType.Login: - sdkCipher.login = this.login.toSdkLogin(); + if (this.login != null) { + sdkCipher.login = this.login.toSdkLogin(); + } break; case CipherType.SecureNote: - sdkCipher.secureNote = this.secureNote.toSdkSecureNote(); + if (this.secureNote != null) { + sdkCipher.secureNote = this.secureNote.toSdkSecureNote(); + } break; case CipherType.Card: - sdkCipher.card = this.card.toSdkCard(); + if (this.card != null) { + sdkCipher.card = this.card.toSdkCard(); + } break; case CipherType.Identity: - sdkCipher.identity = this.identity.toSdkIdentity(); + if (this.identity != null) { + sdkCipher.identity = this.identity.toSdkIdentity(); + } break; case CipherType.SshKey: - sdkCipher.sshKey = this.sshKey.toSdkSshKey(); + if (this.sshKey != null) { + sdkCipher.sshKey = this.sshKey.toSdkSshKey(); + } break; default: break; @@ -413,22 +460,22 @@ export class Cipher extends Domain implements Decryptable { * Maps an SDK Cipher object to a Cipher * @param sdkCipher - The SDK Cipher object */ - static fromSdkCipher(sdkCipher: SdkCipher | null): Cipher | undefined { + static fromSdkCipher(sdkCipher?: SdkCipher): Cipher | undefined { if (sdkCipher == null) { return undefined; } const cipher = new Cipher(); - cipher.id = sdkCipher.id ? uuidAsString(sdkCipher.id) : undefined; + cipher.id = sdkCipher.id ? uuidAsString(sdkCipher.id) : ""; cipher.organizationId = sdkCipher.organizationId ? uuidAsString(sdkCipher.organizationId) : undefined; cipher.folderId = sdkCipher.folderId ? uuidAsString(sdkCipher.folderId) : undefined; cipher.collectionIds = sdkCipher.collectionIds ? sdkCipher.collectionIds.map(uuidAsString) : []; - cipher.key = EncString.fromJSON(sdkCipher.key); + cipher.key = encStringFrom(sdkCipher.key); cipher.name = EncString.fromJSON(sdkCipher.name); - cipher.notes = EncString.fromJSON(sdkCipher.notes); + cipher.notes = encStringFrom(sdkCipher.notes); cipher.type = sdkCipher.type; cipher.favorite = sdkCipher.favorite; cipher.organizationUseTotp = sdkCipher.organizationUseTotp; @@ -436,10 +483,15 @@ export class Cipher extends Domain implements Decryptable { cipher.permissions = CipherPermissionsApi.fromSdkCipherPermissions(sdkCipher.permissions); cipher.viewPassword = sdkCipher.viewPassword; cipher.localData = fromSdkLocalData(sdkCipher.localData); - cipher.attachments = sdkCipher.attachments?.map((a) => Attachment.fromSdkAttachment(a)) ?? []; - cipher.fields = sdkCipher.fields?.map((f) => Field.fromSdkField(f)) ?? []; - cipher.passwordHistory = - sdkCipher.passwordHistory?.map((ph) => Password.fromSdkPasswordHistory(ph)) ?? []; + cipher.attachments = sdkCipher.attachments + ?.map((a) => Attachment.fromSdkAttachment(a)) + .filter((a): a is Attachment => a != null); + cipher.fields = sdkCipher.fields + ?.map((f) => Field.fromSdkField(f)) + .filter((f): f is Field => f != null); + cipher.passwordHistory = sdkCipher.passwordHistory + ?.map((ph) => Password.fromSdkPasswordHistory(ph)) + .filter((ph): ph is Password => ph != null); cipher.creationDate = new Date(sdkCipher.creationDate); cipher.revisionDate = new Date(sdkCipher.revisionDate); cipher.deletedDate = sdkCipher.deletedDate ? new Date(sdkCipher.deletedDate) : undefined; diff --git a/libs/common/src/vault/models/domain/fido2-credential.spec.ts b/libs/common/src/vault/models/domain/fido2-credential.spec.ts index e245e54de7c..3f43775433e 100644 --- a/libs/common/src/vault/models/domain/fido2-credential.spec.ts +++ b/libs/common/src/vault/models/domain/fido2-credential.spec.ts @@ -13,25 +13,23 @@ describe("Fido2Credential", () => { }); describe("constructor", () => { - it("returns all fields null when given empty data parameter", () => { + it("returns all fields undefined when given empty data parameter", () => { const data = new Fido2CredentialData(); const credential = new Fido2Credential(data); - expect(credential).toEqual({ - credentialId: null, - keyType: null, - keyAlgorithm: null, - keyCurve: null, - keyValue: null, - rpId: null, - userHandle: null, - userName: null, - rpName: null, - userDisplayName: null, - counter: null, - discoverable: null, - creationDate: null, - }); + expect(credential.credentialId).toBeDefined(); + expect(credential.keyType).toBeDefined(); + expect(credential.keyAlgorithm).toBeDefined(); + expect(credential.keyCurve).toBeDefined(); + expect(credential.keyValue).toBeDefined(); + expect(credential.rpId).toBeDefined(); + expect(credential.counter).toBeDefined(); + expect(credential.discoverable).toBeDefined(); + expect(credential.userHandle).toBeUndefined(); + expect(credential.userName).toBeUndefined(); + expect(credential.rpName).toBeUndefined(); + expect(credential.userDisplayName).toBeUndefined(); + expect(credential.creationDate).toBeInstanceOf(Date); }); it("returns all fields as EncStrings except creationDate when given full Fido2CredentialData", () => { @@ -69,12 +67,22 @@ describe("Fido2Credential", () => { }); }); - it("should not populate fields when data parameter is not given", () => { + it("should not populate fields when data parameter is not given except creationDate", () => { const credential = new Fido2Credential(); - expect(credential).toEqual({ - credentialId: null, - }); + expect(credential.credentialId).toBeUndefined(); + expect(credential.keyType).toBeUndefined(); + expect(credential.keyAlgorithm).toBeUndefined(); + expect(credential.keyCurve).toBeUndefined(); + expect(credential.keyValue).toBeUndefined(); + expect(credential.rpId).toBeUndefined(); + expect(credential.userHandle).toBeUndefined(); + expect(credential.userName).toBeUndefined(); + expect(credential.counter).toBeUndefined(); + expect(credential.rpName).toBeUndefined(); + expect(credential.userDisplayName).toBeUndefined(); + expect(credential.discoverable).toBeUndefined(); + expect(credential.creationDate).toBeInstanceOf(Date); }); }); @@ -163,8 +171,8 @@ describe("Fido2Credential", () => { expect(result).toEqual(credential); }); - it("returns null if input is null", () => { - expect(Fido2Credential.fromJSON(null)).toBeNull(); + it("returns undefined if input is null", () => { + expect(Fido2Credential.fromJSON(null)).toBeUndefined(); }); }); diff --git a/libs/common/src/vault/models/domain/fido2-credential.ts b/libs/common/src/vault/models/domain/fido2-credential.ts index bdfac9a85ad..eff95c4d0bd 100644 --- a/libs/common/src/vault/models/domain/fido2-credential.ts +++ b/libs/common/src/vault/models/domain/fido2-credential.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Jsonify } from "type-fest"; import { Fido2Credential as SdkFido2Credential } from "@bitwarden/sdk-internal"; @@ -7,56 +5,53 @@ import { Fido2Credential as SdkFido2Credential } from "@bitwarden/sdk-internal"; import { EncString } from "../../../key-management/crypto/models/enc-string"; import Domain from "../../../platform/models/domain/domain-base"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; +import { conditionalEncString, encStringFrom } from "../../utils/domain-utils"; import { Fido2CredentialData } from "../data/fido2-credential.data"; import { Fido2CredentialView } from "../view/fido2-credential.view"; export class Fido2Credential extends Domain { - credentialId: EncString | null = null; - keyType: EncString; - keyAlgorithm: EncString; - keyCurve: EncString; - keyValue: EncString; - rpId: EncString; - userHandle: EncString; - userName: EncString; - counter: EncString; - rpName: EncString; - userDisplayName: EncString; - discoverable: EncString; - creationDate: Date; + credentialId!: EncString; + keyType!: EncString; + keyAlgorithm!: EncString; + keyCurve!: EncString; + keyValue!: EncString; + rpId!: EncString; + userHandle?: EncString; + userName?: EncString; + counter!: EncString; + rpName?: EncString; + userDisplayName?: EncString; + discoverable!: EncString; + creationDate!: Date; constructor(obj?: Fido2CredentialData) { super(); if (obj == null) { + this.creationDate = new Date(); return; } - this.buildDomainModel( - this, - obj, - { - credentialId: null, - keyType: null, - keyAlgorithm: null, - keyCurve: null, - keyValue: null, - rpId: null, - userHandle: null, - userName: null, - counter: null, - rpName: null, - userDisplayName: null, - discoverable: null, - }, - [], - ); - this.creationDate = obj.creationDate != null ? new Date(obj.creationDate) : null; + this.credentialId = new EncString(obj.credentialId); + this.keyType = new EncString(obj.keyType); + this.keyAlgorithm = new EncString(obj.keyAlgorithm); + this.keyCurve = new EncString(obj.keyCurve); + this.keyValue = new EncString(obj.keyValue); + this.rpId = new EncString(obj.rpId); + this.counter = new EncString(obj.counter); + this.discoverable = new EncString(obj.discoverable); + this.userHandle = conditionalEncString(obj.userHandle); + this.userName = conditionalEncString(obj.userName); + this.rpName = conditionalEncString(obj.rpName); + this.userDisplayName = conditionalEncString(obj.userDisplayName); + this.creationDate = new Date(obj.creationDate); } - async decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { + async decrypt( + orgId: string | undefined, + encKey?: SymmetricCryptoKey, + ): Promise { const view = await this.decryptObj( this, - // @ts-expect-error ViewEncryptableKeys type should be fixed to allow for optional values, but is out of scope for now. new Fido2CredentialView(), [ "credentialId", @@ -70,7 +65,7 @@ export class Fido2Credential extends Domain { "rpName", "userDisplayName", ], - orgId, + orgId ?? null, encKey, ); @@ -79,7 +74,7 @@ export class Fido2Credential extends Domain { { counter: string; } - >(this, { counter: "" }, ["counter"], orgId, encKey); + >(this, { counter: "" }, ["counter"], orgId ?? null, encKey); // Counter will end up as NaN if this fails view.counter = parseInt(counter); @@ -87,7 +82,7 @@ export class Fido2Credential extends Domain { this, { discoverable: "" }, ["discoverable"], - orgId, + orgId ?? null, encKey, ); view.discoverable = discoverable === "true"; @@ -116,40 +111,28 @@ export class Fido2Credential extends Domain { return i; } - static fromJSON(obj: Jsonify): Fido2Credential { + static fromJSON(obj: Jsonify | undefined): Fido2Credential | undefined { if (obj == null) { - return null; + return undefined; } - const credentialId = EncString.fromJSON(obj.credentialId); - const keyType = EncString.fromJSON(obj.keyType); - const keyAlgorithm = EncString.fromJSON(obj.keyAlgorithm); - const keyCurve = EncString.fromJSON(obj.keyCurve); - const keyValue = EncString.fromJSON(obj.keyValue); - const rpId = EncString.fromJSON(obj.rpId); - const userHandle = EncString.fromJSON(obj.userHandle); - const userName = EncString.fromJSON(obj.userName); - const counter = EncString.fromJSON(obj.counter); - const rpName = EncString.fromJSON(obj.rpName); - const userDisplayName = EncString.fromJSON(obj.userDisplayName); - const discoverable = EncString.fromJSON(obj.discoverable); - const creationDate = obj.creationDate != null ? new Date(obj.creationDate) : null; + const credential = new Fido2Credential(); - return Object.assign(new Fido2Credential(), obj, { - credentialId, - keyType, - keyAlgorithm, - keyCurve, - keyValue, - rpId, - userHandle, - userName, - counter, - rpName, - userDisplayName, - discoverable, - creationDate, - }); + credential.credentialId = EncString.fromJSON(obj.credentialId); + credential.keyType = EncString.fromJSON(obj.keyType); + credential.keyAlgorithm = EncString.fromJSON(obj.keyAlgorithm); + credential.keyCurve = EncString.fromJSON(obj.keyCurve); + credential.keyValue = EncString.fromJSON(obj.keyValue); + credential.rpId = EncString.fromJSON(obj.rpId); + credential.userHandle = encStringFrom(obj.userHandle); + credential.userName = encStringFrom(obj.userName); + credential.counter = EncString.fromJSON(obj.counter); + credential.rpName = encStringFrom(obj.rpName); + credential.userDisplayName = encStringFrom(obj.userDisplayName); + credential.discoverable = EncString.fromJSON(obj.discoverable); + credential.creationDate = new Date(obj.creationDate); + + return credential; } /** @@ -179,8 +162,8 @@ export class Fido2Credential extends Domain { * Maps an SDK Fido2Credential object to a Fido2Credential * @param obj - The SDK Fido2Credential object */ - static fromSdkFido2Credential(obj: SdkFido2Credential): Fido2Credential | undefined { - if (!obj) { + static fromSdkFido2Credential(obj?: SdkFido2Credential): Fido2Credential | undefined { + if (obj == null) { return undefined; } @@ -192,11 +175,11 @@ export class Fido2Credential extends Domain { credential.keyCurve = EncString.fromJSON(obj.keyCurve); credential.keyValue = EncString.fromJSON(obj.keyValue); credential.rpId = EncString.fromJSON(obj.rpId); - credential.userHandle = EncString.fromJSON(obj.userHandle); - credential.userName = EncString.fromJSON(obj.userName); credential.counter = EncString.fromJSON(obj.counter); - credential.rpName = EncString.fromJSON(obj.rpName); - credential.userDisplayName = EncString.fromJSON(obj.userDisplayName); + credential.userHandle = encStringFrom(obj.userHandle); + credential.userName = encStringFrom(obj.userName); + credential.rpName = encStringFrom(obj.rpName); + credential.userDisplayName = encStringFrom(obj.userDisplayName); credential.discoverable = EncString.fromJSON(obj.discoverable); credential.creationDate = new Date(obj.creationDate); diff --git a/libs/common/src/vault/models/domain/field.spec.ts b/libs/common/src/vault/models/domain/field.spec.ts index b5e26199e7d..d99336adad0 100644 --- a/libs/common/src/vault/models/domain/field.spec.ts +++ b/libs/common/src/vault/models/domain/field.spec.ts @@ -30,8 +30,8 @@ describe("Field", () => { expect(field).toEqual({ type: undefined, - name: null, - value: null, + name: undefined, + value: undefined, linkedId: undefined, }); }); @@ -41,9 +41,9 @@ describe("Field", () => { expect(field).toEqual({ type: FieldType.Text, - name: { encryptedString: "encName", encryptionType: 0 }, - value: { encryptedString: "encValue", encryptionType: 0 }, - linkedId: null, + name: new EncString("encName"), + value: new EncString("encValue"), + linkedId: undefined, }); }); @@ -82,12 +82,14 @@ describe("Field", () => { expect(actual).toEqual({ name: "myName_fromJSON", value: "myValue_fromJSON", + type: FieldType.Text, + linkedId: undefined, }); expect(actual).toBeInstanceOf(Field); }); - it("returns null if object is null", () => { - expect(Field.fromJSON(null)).toBeNull(); + it("returns undefined if object is null", () => { + expect(Field.fromJSON(null)).toBeUndefined(); }); }); diff --git a/libs/common/src/vault/models/domain/field.ts b/libs/common/src/vault/models/domain/field.ts index 130d1cc56d5..2ee3a9af8a5 100644 --- a/libs/common/src/vault/models/domain/field.ts +++ b/libs/common/src/vault/models/domain/field.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Jsonify } from "type-fest"; import { Field as SdkField, LinkedIdType as SdkLinkedIdType } from "@bitwarden/sdk-internal"; @@ -8,14 +6,15 @@ import { EncString } from "../../../key-management/crypto/models/enc-string"; import Domain from "../../../platform/models/domain/domain-base"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { FieldType, LinkedIdType } from "../../enums"; +import { conditionalEncString, encStringFrom } from "../../utils/domain-utils"; import { FieldData } from "../data/field.data"; import { FieldView } from "../view/field.view"; export class Field extends Domain { - name: EncString; - value: EncString; - type: FieldType; - linkedId: LinkedIdType; + name?: EncString; + value?: EncString; + type: FieldType = FieldType.Text; + linkedId?: LinkedIdType; constructor(obj?: FieldData) { super(); @@ -24,25 +23,17 @@ export class Field extends Domain { } this.type = obj.type; - this.linkedId = obj.linkedId; - this.buildDomainModel( - this, - obj, - { - name: null, - value: null, - }, - [], - ); + this.linkedId = obj.linkedId ?? undefined; + this.name = conditionalEncString(obj.name); + this.value = conditionalEncString(obj.value); } - decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { + decrypt(orgId: string | undefined, encKey?: SymmetricCryptoKey): Promise { return this.decryptObj( this, - // @ts-expect-error ViewEncryptableKeys type should be fixed to allow for optional values, but is out of scope for now. new FieldView(this), ["name", "value"], - orgId, + orgId ?? null, encKey, ); } @@ -63,18 +54,18 @@ export class Field extends Domain { return f; } - static fromJSON(obj: Partial>): Field { + static fromJSON(obj: Partial> | undefined): Field | undefined { if (obj == null) { - return null; + return undefined; } - const name = EncString.fromJSON(obj.name); - const value = EncString.fromJSON(obj.value); + const field = new Field(); + field.type = obj.type ?? FieldType.Text; + field.linkedId = obj.linkedId ?? undefined; + field.name = encStringFrom(obj.name); + field.value = encStringFrom(obj.value); - return Object.assign(new Field(), obj, { - name, - value, - }); + return field; } /** @@ -96,14 +87,14 @@ export class Field extends Domain { * Maps SDK Field to Field * @param obj The SDK Field object to map */ - static fromSdkField(obj: SdkField): Field | undefined { - if (!obj) { + static fromSdkField(obj?: SdkField): Field | undefined { + if (obj == null) { return undefined; } const field = new Field(); - field.name = EncString.fromJSON(obj.name); - field.value = EncString.fromJSON(obj.value); + field.name = encStringFrom(obj.name); + field.value = encStringFrom(obj.value); field.type = obj.type; field.linkedId = obj.linkedId; diff --git a/libs/common/src/vault/models/domain/identity.spec.ts b/libs/common/src/vault/models/domain/identity.spec.ts index 9fbcb92e4ae..c2c2363fa0d 100644 --- a/libs/common/src/vault/models/domain/identity.spec.ts +++ b/libs/common/src/vault/models/domain/identity.spec.ts @@ -34,24 +34,24 @@ describe("Identity", () => { const identity = new Identity(data); expect(identity).toEqual({ - address1: null, - address2: null, - address3: null, - city: null, - company: null, - country: null, - email: null, - firstName: null, - lastName: null, - licenseNumber: null, - middleName: null, - passportNumber: null, - phone: null, - postalCode: null, - ssn: null, - state: null, - title: null, - username: null, + address1: undefined, + address2: undefined, + address3: undefined, + city: undefined, + company: undefined, + country: undefined, + email: undefined, + firstName: undefined, + lastName: undefined, + licenseNumber: undefined, + middleName: undefined, + passportNumber: undefined, + phone: undefined, + postalCode: undefined, + ssn: undefined, + state: undefined, + title: undefined, + username: undefined, }); }); @@ -179,8 +179,8 @@ describe("Identity", () => { expect(actual).toBeInstanceOf(Identity); }); - it("returns null if object is null", () => { - expect(Identity.fromJSON(null)).toBeNull(); + it("returns undefined if object is null", () => { + expect(Identity.fromJSON(null)).toBeUndefined(); }); }); diff --git a/libs/common/src/vault/models/domain/identity.ts b/libs/common/src/vault/models/domain/identity.ts index f0d5b3123ab..e2def3eb386 100644 --- a/libs/common/src/vault/models/domain/identity.ts +++ b/libs/common/src/vault/models/domain/identity.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Jsonify } from "type-fest"; import { Identity as SdkIdentity } from "@bitwarden/sdk-internal"; @@ -7,28 +5,29 @@ import { Identity as SdkIdentity } from "@bitwarden/sdk-internal"; import { EncString } from "../../../key-management/crypto/models/enc-string"; import Domain from "../../../platform/models/domain/domain-base"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; +import { conditionalEncString, encStringFrom } from "../../utils/domain-utils"; import { IdentityData } from "../data/identity.data"; import { IdentityView } from "../view/identity.view"; export class Identity extends Domain { - title: EncString; - firstName: EncString; - middleName: EncString; - lastName: EncString; - address1: EncString; - address2: EncString; - address3: EncString; - city: EncString; - state: EncString; - postalCode: EncString; - country: EncString; - company: EncString; - email: EncString; - phone: EncString; - ssn: EncString; - username: EncString; - passportNumber: EncString; - licenseNumber: EncString; + title?: EncString; + firstName?: EncString; + middleName?: EncString; + lastName?: EncString; + address1?: EncString; + address2?: EncString; + address3?: EncString; + city?: EncString; + state?: EncString; + postalCode?: EncString; + country?: EncString; + company?: EncString; + email?: EncString; + phone?: EncString; + ssn?: EncString; + username?: EncString; + passportNumber?: EncString; + licenseNumber?: EncString; constructor(obj?: IdentityData) { super(); @@ -36,35 +35,28 @@ export class Identity extends Domain { return; } - this.buildDomainModel( - this, - obj, - { - title: null, - firstName: null, - middleName: null, - lastName: null, - address1: null, - address2: null, - address3: null, - city: null, - state: null, - postalCode: null, - country: null, - company: null, - email: null, - phone: null, - ssn: null, - username: null, - passportNumber: null, - licenseNumber: null, - }, - [], - ); + this.title = conditionalEncString(obj.title); + this.firstName = conditionalEncString(obj.firstName); + this.middleName = conditionalEncString(obj.middleName); + this.lastName = conditionalEncString(obj.lastName); + this.address1 = conditionalEncString(obj.address1); + this.address2 = conditionalEncString(obj.address2); + this.address3 = conditionalEncString(obj.address3); + this.city = conditionalEncString(obj.city); + this.state = conditionalEncString(obj.state); + this.postalCode = conditionalEncString(obj.postalCode); + this.country = conditionalEncString(obj.country); + this.company = conditionalEncString(obj.company); + this.email = conditionalEncString(obj.email); + this.phone = conditionalEncString(obj.phone); + this.ssn = conditionalEncString(obj.ssn); + this.username = conditionalEncString(obj.username); + this.passportNumber = conditionalEncString(obj.passportNumber); + this.licenseNumber = conditionalEncString(obj.licenseNumber); } decrypt( - orgId: string, + orgId: string | undefined, context: string = "No Cipher Context", encKey?: SymmetricCryptoKey, ): Promise { @@ -91,7 +83,7 @@ export class Identity extends Domain { "passportNumber", "licenseNumber", ], - orgId, + orgId ?? null, encKey, "DomainType: Identity; " + context, ); @@ -122,50 +114,32 @@ export class Identity extends Domain { return i; } - static fromJSON(obj: Jsonify): Identity { + static fromJSON(obj: Jsonify | undefined): Identity | undefined { if (obj == null) { - return null; + return undefined; } - const title = EncString.fromJSON(obj.title); - const firstName = EncString.fromJSON(obj.firstName); - const middleName = EncString.fromJSON(obj.middleName); - const lastName = EncString.fromJSON(obj.lastName); - const address1 = EncString.fromJSON(obj.address1); - const address2 = EncString.fromJSON(obj.address2); - const address3 = EncString.fromJSON(obj.address3); - const city = EncString.fromJSON(obj.city); - const state = EncString.fromJSON(obj.state); - const postalCode = EncString.fromJSON(obj.postalCode); - const country = EncString.fromJSON(obj.country); - const company = EncString.fromJSON(obj.company); - const email = EncString.fromJSON(obj.email); - const phone = EncString.fromJSON(obj.phone); - const ssn = EncString.fromJSON(obj.ssn); - const username = EncString.fromJSON(obj.username); - const passportNumber = EncString.fromJSON(obj.passportNumber); - const licenseNumber = EncString.fromJSON(obj.licenseNumber); + const identity = new Identity(); + identity.title = encStringFrom(obj.title); + identity.firstName = encStringFrom(obj.firstName); + identity.middleName = encStringFrom(obj.middleName); + identity.lastName = encStringFrom(obj.lastName); + identity.address1 = encStringFrom(obj.address1); + identity.address2 = encStringFrom(obj.address2); + identity.address3 = encStringFrom(obj.address3); + identity.city = encStringFrom(obj.city); + identity.state = encStringFrom(obj.state); + identity.postalCode = encStringFrom(obj.postalCode); + identity.country = encStringFrom(obj.country); + identity.company = encStringFrom(obj.company); + identity.email = encStringFrom(obj.email); + identity.phone = encStringFrom(obj.phone); + identity.ssn = encStringFrom(obj.ssn); + identity.username = encStringFrom(obj.username); + identity.passportNumber = encStringFrom(obj.passportNumber); + identity.licenseNumber = encStringFrom(obj.licenseNumber); - return Object.assign(new Identity(), obj, { - title, - firstName, - middleName, - lastName, - address1, - address2, - address3, - city, - state, - postalCode, - country, - company, - email, - phone, - ssn, - username, - passportNumber, - licenseNumber, - }); + return identity; } /** @@ -200,30 +174,30 @@ export class Identity extends Domain { * Maps an SDK Identity object to an Identity * @param obj - The SDK Identity object */ - static fromSdkIdentity(obj: SdkIdentity): Identity | undefined { + static fromSdkIdentity(obj?: SdkIdentity): Identity | undefined { if (obj == null) { return undefined; } const identity = new Identity(); - identity.title = EncString.fromJSON(obj.title); - identity.firstName = EncString.fromJSON(obj.firstName); - identity.middleName = EncString.fromJSON(obj.middleName); - identity.lastName = EncString.fromJSON(obj.lastName); - identity.address1 = EncString.fromJSON(obj.address1); - identity.address2 = EncString.fromJSON(obj.address2); - identity.address3 = EncString.fromJSON(obj.address3); - identity.city = EncString.fromJSON(obj.city); - identity.state = EncString.fromJSON(obj.state); - identity.postalCode = EncString.fromJSON(obj.postalCode); - identity.country = EncString.fromJSON(obj.country); - identity.company = EncString.fromJSON(obj.company); - identity.email = EncString.fromJSON(obj.email); - identity.phone = EncString.fromJSON(obj.phone); - identity.ssn = EncString.fromJSON(obj.ssn); - identity.username = EncString.fromJSON(obj.username); - identity.passportNumber = EncString.fromJSON(obj.passportNumber); - identity.licenseNumber = EncString.fromJSON(obj.licenseNumber); + identity.title = encStringFrom(obj.title); + identity.firstName = encStringFrom(obj.firstName); + identity.middleName = encStringFrom(obj.middleName); + identity.lastName = encStringFrom(obj.lastName); + identity.address1 = encStringFrom(obj.address1); + identity.address2 = encStringFrom(obj.address2); + identity.address3 = encStringFrom(obj.address3); + identity.city = encStringFrom(obj.city); + identity.state = encStringFrom(obj.state); + identity.postalCode = encStringFrom(obj.postalCode); + identity.country = encStringFrom(obj.country); + identity.company = encStringFrom(obj.company); + identity.email = encStringFrom(obj.email); + identity.phone = encStringFrom(obj.phone); + identity.ssn = encStringFrom(obj.ssn); + identity.username = encStringFrom(obj.username); + identity.passportNumber = encStringFrom(obj.passportNumber); + identity.licenseNumber = encStringFrom(obj.licenseNumber); return identity; } diff --git a/libs/common/src/vault/models/domain/login-uri.spec.ts b/libs/common/src/vault/models/domain/login-uri.spec.ts index e67ba771412..982b435384b 100644 --- a/libs/common/src/vault/models/domain/login-uri.spec.ts +++ b/libs/common/src/vault/models/domain/login-uri.spec.ts @@ -27,9 +27,9 @@ describe("LoginUri", () => { const loginUri = new LoginUri(data); expect(loginUri).toEqual({ - match: null, - uri: null, - uriChecksum: null, + match: undefined, + uri: undefined, + uriChecksum: undefined, }); }); @@ -77,7 +77,7 @@ describe("LoginUri", () => { loginUri.uriChecksum = mockEnc("checksum"); encryptService.hash.mockResolvedValue("checksum"); - const actual = await loginUri.validateChecksum("uri", null, null); + const actual = await loginUri.validateChecksum("uri", undefined, undefined); expect(actual).toBe(true); expect(encryptService.hash).toHaveBeenCalledWith("uri", "sha256"); @@ -88,7 +88,7 @@ describe("LoginUri", () => { loginUri.uriChecksum = mockEnc("checksum"); encryptService.hash.mockResolvedValue("incorrect checksum"); - const actual = await loginUri.validateChecksum("uri", null, null); + const actual = await loginUri.validateChecksum("uri", undefined, undefined); expect(actual).toBe(false); }); @@ -112,8 +112,8 @@ describe("LoginUri", () => { expect(actual).toBeInstanceOf(LoginUri); }); - it("returns null if object is null", () => { - expect(LoginUri.fromJSON(null)).toBeNull(); + it("returns undefined if object is null", () => { + expect(LoginUri.fromJSON(null)).toBeUndefined(); }); }); diff --git a/libs/common/src/vault/models/domain/login-uri.ts b/libs/common/src/vault/models/domain/login-uri.ts index 973e25c8ff1..cac487747f8 100644 --- a/libs/common/src/vault/models/domain/login-uri.ts +++ b/libs/common/src/vault/models/domain/login-uri.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Jsonify } from "type-fest"; import { LoginUri as SdkLoginUri } from "@bitwarden/sdk-internal"; @@ -9,13 +7,14 @@ import { UriMatchStrategySetting } from "../../../models/domain/domain-service"; import { Utils } from "../../../platform/misc/utils"; import Domain from "../../../platform/models/domain/domain-base"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; +import { conditionalEncString, encStringFrom } from "../../utils/domain-utils"; import { LoginUriData } from "../data/login-uri.data"; import { LoginUriView } from "../view/login-uri.view"; export class LoginUri extends Domain { - uri: EncString; - uriChecksum: EncString | undefined; - match: UriMatchStrategySetting; + uri?: EncString; + uriChecksum?: EncString; + match?: UriMatchStrategySetting; constructor(obj?: LoginUriData) { super(); @@ -23,20 +22,13 @@ export class LoginUri extends Domain { return; } - this.match = obj.match; - this.buildDomainModel( - this, - obj, - { - uri: null, - uriChecksum: null, - }, - [], - ); + this.uri = conditionalEncString(obj.uri); + this.uriChecksum = conditionalEncString(obj.uriChecksum); + this.match = obj.match ?? undefined; } decrypt( - orgId: string, + orgId: string | undefined, context: string = "No Cipher Context", encKey?: SymmetricCryptoKey, ): Promise { @@ -44,13 +36,13 @@ export class LoginUri extends Domain { this, new LoginUriView(this), ["uri"], - orgId, + orgId ?? null, encKey, context, ); } - async validateChecksum(clearTextUri: string, orgId: string, encKey: SymmetricCryptoKey) { + async validateChecksum(clearTextUri: string, orgId?: string, encKey?: SymmetricCryptoKey) { if (this.uriChecksum == null) { return false; } @@ -58,7 +50,7 @@ export class LoginUri extends Domain { const keyService = Utils.getContainerService().getEncryptService(); const localChecksum = await keyService.hash(clearTextUri, "sha256"); - const remoteChecksum = await this.uriChecksum.decrypt(orgId, encKey); + const remoteChecksum = await this.uriChecksum.decrypt(orgId ?? null, encKey); return remoteChecksum === localChecksum; } @@ -77,17 +69,17 @@ export class LoginUri extends Domain { return u; } - static fromJSON(obj: Jsonify): LoginUri { + static fromJSON(obj: Jsonify | undefined): LoginUri | undefined { if (obj == null) { - return null; + return undefined; } - const uri = EncString.fromJSON(obj.uri); - const uriChecksum = EncString.fromJSON(obj.uriChecksum); - return Object.assign(new LoginUri(), obj, { - uri, - uriChecksum, - }); + const loginUri = new LoginUri(); + loginUri.uri = encStringFrom(obj.uri); + loginUri.match = obj.match ?? undefined; + loginUri.uriChecksum = encStringFrom(obj.uriChecksum); + + return loginUri; } /** @@ -103,16 +95,16 @@ export class LoginUri extends Domain { }; } - static fromSdkLoginUri(obj: SdkLoginUri): LoginUri | undefined { + static fromSdkLoginUri(obj?: SdkLoginUri): LoginUri | undefined { if (obj == null) { return undefined; } - const view = new LoginUri(); - view.uri = EncString.fromJSON(obj.uri); - view.uriChecksum = obj.uriChecksum ? EncString.fromJSON(obj.uriChecksum) : undefined; - view.match = obj.match; + const loginUri = new LoginUri(); + loginUri.uri = encStringFrom(obj.uri); + loginUri.uriChecksum = encStringFrom(obj.uriChecksum); + loginUri.match = obj.match; - return view; + return loginUri; } } diff --git a/libs/common/src/vault/models/domain/login.spec.ts b/libs/common/src/vault/models/domain/login.spec.ts index 99ceb2b0a3d..9f03e225b7f 100644 --- a/libs/common/src/vault/models/domain/login.spec.ts +++ b/libs/common/src/vault/models/domain/login.spec.ts @@ -19,11 +19,11 @@ describe("Login DTO", () => { const login = new Login(data); expect(login).toEqual({ - passwordRevisionDate: null, + passwordRevisionDate: undefined, autofillOnPageLoad: undefined, - username: null, - password: null, - totp: null, + username: undefined, + password: undefined, + totp: undefined, }); }); @@ -193,8 +193,8 @@ describe("Login DTO", () => { expect(actual).toBeInstanceOf(Login); }); - it("returns null if object is null", () => { - expect(Login.fromJSON(null)).toBeNull(); + it("returns undefined if object is null", () => { + expect(Login.fromJSON(null)).toBeUndefined(); }); }); diff --git a/libs/common/src/vault/models/domain/login.ts b/libs/common/src/vault/models/domain/login.ts index b34fb011254..13342c69014 100644 --- a/libs/common/src/vault/models/domain/login.ts +++ b/libs/common/src/vault/models/domain/login.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Jsonify } from "type-fest"; import { Login as SdkLogin } from "@bitwarden/sdk-internal"; @@ -7,6 +5,7 @@ import { Login as SdkLogin } from "@bitwarden/sdk-internal"; import { EncString } from "../../../key-management/crypto/models/enc-string"; import Domain from "../../../platform/models/domain/domain-base"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; +import { conditionalEncString, encStringFrom } from "../../utils/domain-utils"; import { LoginData } from "../data/login.data"; import { LoginView } from "../view/login.view"; @@ -14,13 +13,13 @@ import { Fido2Credential } from "./fido2-credential"; import { LoginUri } from "./login-uri"; export class Login extends Domain { - uris: LoginUri[]; - username: EncString; - password: EncString; + uris?: LoginUri[]; + username?: EncString; + password?: EncString; passwordRevisionDate?: Date; - totp: EncString; - autofillOnPageLoad: boolean; - fido2Credentials: Fido2Credential[]; + totp?: EncString; + autofillOnPageLoad?: boolean; + fido2Credentials?: Fido2Credential[]; constructor(obj?: LoginData) { super(); @@ -29,24 +28,14 @@ export class Login extends Domain { } this.passwordRevisionDate = - obj.passwordRevisionDate != null ? new Date(obj.passwordRevisionDate) : null; + obj.passwordRevisionDate != null ? new Date(obj.passwordRevisionDate) : undefined; this.autofillOnPageLoad = obj.autofillOnPageLoad; - this.buildDomainModel( - this, - obj, - { - username: null, - password: null, - totp: null, - }, - [], - ); + this.username = conditionalEncString(obj.username); + this.password = conditionalEncString(obj.password); + this.totp = conditionalEncString(obj.totp); if (obj.uris) { - this.uris = []; - obj.uris.forEach((u) => { - this.uris.push(new LoginUri(u)); - }); + this.uris = obj.uris.map((u) => new LoginUri(u)); } if (obj.fido2Credentials) { @@ -55,7 +44,7 @@ export class Login extends Domain { } async decrypt( - orgId: string, + orgId: string | undefined, bypassValidation: boolean, context: string = "No Cipher Context", encKey?: SymmetricCryptoKey, @@ -64,7 +53,7 @@ export class Login extends Domain { this, new LoginView(this), ["username", "password", "totp"], - orgId, + orgId ?? null, encKey, `DomainType: Login; ${context}`, ); @@ -78,12 +67,21 @@ export class Login extends Domain { } const uri = await this.uris[i].decrypt(orgId, context, encKey); + const uriString = uri.uri; + + if (uriString == null) { + continue; + } + // URIs are shared remotely after decryption // we need to validate that the string hasn't been changed by a compromised server // This validation is tied to the existence of cypher.key for backwards compatibility - // So we bypass the validation if there's no cipher.key or procceed with the validation and + // So we bypass the validation if there's no cipher.key or proceed with the validation and // Skip the value if it's been tampered with. - if (bypassValidation || (await this.uris[i].validateChecksum(uri.uri, orgId, encKey))) { + const isValidUri = + bypassValidation || (await this.uris[i].validateChecksum(uriString, orgId, encKey)); + + if (isValidUri) { view.uris.push(uri); } } @@ -100,9 +98,12 @@ export class Login extends Domain { toLoginData(): LoginData { const l = new LoginData(); - l.passwordRevisionDate = - this.passwordRevisionDate != null ? this.passwordRevisionDate.toISOString() : null; - l.autofillOnPageLoad = this.autofillOnPageLoad; + if (this.passwordRevisionDate != null) { + l.passwordRevisionDate = this.passwordRevisionDate.toISOString(); + } + if (this.autofillOnPageLoad != null) { + l.autofillOnPageLoad = this.autofillOnPageLoad; + } this.buildDataModel(this, l, { username: null, password: null, @@ -123,28 +124,27 @@ export class Login extends Domain { return l; } - static fromJSON(obj: Partial>): Login { + static fromJSON(obj: Partial> | undefined): Login | undefined { if (obj == null) { - return null; + return undefined; } - const username = EncString.fromJSON(obj.username); - const password = EncString.fromJSON(obj.password); - const totp = EncString.fromJSON(obj.totp); - const passwordRevisionDate = - obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate); - const uris = obj.uris?.map((uri: any) => LoginUri.fromJSON(uri)); - const fido2Credentials = - obj.fido2Credentials?.map((key) => Fido2Credential.fromJSON(key)) ?? []; + const login = new Login(); + login.passwordRevisionDate = + obj.passwordRevisionDate != null ? new Date(obj.passwordRevisionDate) : undefined; + login.autofillOnPageLoad = obj.autofillOnPageLoad; + login.username = encStringFrom(obj.username); + login.password = encStringFrom(obj.password); + login.totp = encStringFrom(obj.totp); + login.uris = obj.uris + ?.map((uri: any) => LoginUri.fromJSON(uri)) + .filter((u): u is LoginUri => u != null); + login.fido2Credentials = + obj.fido2Credentials + ?.map((key) => Fido2Credential.fromJSON(key)) + .filter((c): c is Fido2Credential => c != null) ?? undefined; - return Object.assign(new Login(), obj, { - username, - password, - totp, - passwordRevisionDate, - uris, - fido2Credentials, - }); + return login; } /** @@ -168,25 +168,27 @@ export class Login extends Domain { * Maps an SDK Login object to a Login * @param obj - The SDK Login object */ - static fromSdkLogin(obj: SdkLogin): Login | undefined { + static fromSdkLogin(obj?: SdkLogin): Login | undefined { if (!obj) { return undefined; } const login = new Login(); - - login.uris = - obj.uris?.filter((u) => u.uri != null).map((uri) => LoginUri.fromSdkLoginUri(uri)) ?? []; - login.username = EncString.fromJSON(obj.username); - login.password = EncString.fromJSON(obj.password); - login.passwordRevisionDate = obj.passwordRevisionDate - ? new Date(obj.passwordRevisionDate) - : undefined; - login.totp = EncString.fromJSON(obj.totp); + login.passwordRevisionDate = + obj.passwordRevisionDate != null ? new Date(obj.passwordRevisionDate) : undefined; login.autofillOnPageLoad = obj.autofillOnPageLoad; - login.fido2Credentials = obj.fido2Credentials?.map((f) => - Fido2Credential.fromSdkFido2Credential(f), - ); + login.username = encStringFrom(obj.username); + login.password = encStringFrom(obj.password); + login.totp = encStringFrom(obj.totp); + login.uris = + obj.uris + ?.filter((u) => u.uri != null) + .map((uri) => LoginUri.fromSdkLoginUri(uri)) + .filter((u): u is LoginUri => u != null) ?? undefined; + login.fido2Credentials = + obj.fido2Credentials + ?.map((f) => Fido2Credential.fromSdkFido2Credential(f)) + .filter((c): c is Fido2Credential => c != null) ?? undefined; return login; } diff --git a/libs/common/src/vault/models/domain/password.spec.ts b/libs/common/src/vault/models/domain/password.spec.ts index a75fca048fe..2e37c5e8375 100644 --- a/libs/common/src/vault/models/domain/password.spec.ts +++ b/libs/common/src/vault/models/domain/password.spec.ts @@ -17,9 +17,9 @@ describe("Password", () => { const data = new PasswordHistoryData(); const password = new Password(data); - expect(password).toMatchObject({ - password: null, - }); + expect(password).toBeInstanceOf(Password); + expect(password.password).toBeInstanceOf(EncString); + expect(password.lastUsedDate).toBeInstanceOf(Date); }); it("Convert", () => { @@ -66,8 +66,8 @@ describe("Password", () => { expect(actual).toBeInstanceOf(Password); }); - it("returns null if object is null", () => { - expect(Password.fromJSON(null)).toBeNull(); + it("returns undefined if object is null", () => { + expect(Password.fromJSON(null)).toBeUndefined(); }); }); diff --git a/libs/common/src/vault/models/domain/password.ts b/libs/common/src/vault/models/domain/password.ts index ca594075e0b..84e8919b905 100644 --- a/libs/common/src/vault/models/domain/password.ts +++ b/libs/common/src/vault/models/domain/password.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Jsonify } from "type-fest"; import { PasswordHistory } from "@bitwarden/sdk-internal"; @@ -11,8 +9,8 @@ import { PasswordHistoryData } from "../data/password-history.data"; import { PasswordHistoryView } from "../view/password-history.view"; export class Password extends Domain { - password: EncString; - lastUsedDate: Date; + password!: EncString; + lastUsedDate!: Date; constructor(obj?: PasswordHistoryData) { super(); @@ -20,18 +18,16 @@ export class Password extends Domain { return; } - this.buildDomainModel(this, obj, { - password: null, - }); + this.password = new EncString(obj.password); this.lastUsedDate = new Date(obj.lastUsedDate); } - decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { + decrypt(orgId: string | undefined, encKey?: SymmetricCryptoKey): Promise { return this.decryptObj( this, new PasswordHistoryView(this), ["password"], - orgId, + orgId ?? null, encKey, "DomainType: PasswordHistory", ); @@ -46,18 +42,16 @@ export class Password extends Domain { return ph; } - static fromJSON(obj: Partial>): Password { + static fromJSON(obj: Jsonify | undefined): Password | undefined { if (obj == null) { - return null; + return undefined; } - const password = EncString.fromJSON(obj.password); - const lastUsedDate = obj.lastUsedDate == null ? null : new Date(obj.lastUsedDate); + const passwordHistory = new Password(); + passwordHistory.password = EncString.fromJSON(obj.password); + passwordHistory.lastUsedDate = new Date(obj.lastUsedDate); - return Object.assign(new Password(), obj, { - password, - lastUsedDate, - }); + return passwordHistory; } /** @@ -76,7 +70,7 @@ export class Password extends Domain { * Maps an SDK PasswordHistory object to a Password * @param obj - The SDK PasswordHistory object */ - static fromSdkPasswordHistory(obj: PasswordHistory): Password | undefined { + static fromSdkPasswordHistory(obj?: PasswordHistory): Password | undefined { if (!obj) { return undefined; } diff --git a/libs/common/src/vault/models/domain/secure-note.spec.ts b/libs/common/src/vault/models/domain/secure-note.spec.ts index ff71e53238d..4c8e8d470ca 100644 --- a/libs/common/src/vault/models/domain/secure-note.spec.ts +++ b/libs/common/src/vault/models/domain/secure-note.spec.ts @@ -38,7 +38,7 @@ describe("SecureNote", () => { const secureNote = new SecureNote(); secureNote.type = SecureNoteType.Generic; - const view = await secureNote.decrypt(null); + const view = await secureNote.decrypt(); expect(view).toEqual({ type: 0, @@ -46,8 +46,8 @@ describe("SecureNote", () => { }); describe("fromJSON", () => { - it("returns null if object is null", () => { - expect(SecureNote.fromJSON(null)).toBeNull(); + it("returns undefined if object is null", () => { + expect(SecureNote.fromJSON(null)).toBeUndefined(); }); }); diff --git a/libs/common/src/vault/models/domain/secure-note.ts b/libs/common/src/vault/models/domain/secure-note.ts index 1426ff85eab..fb568f482b0 100644 --- a/libs/common/src/vault/models/domain/secure-note.ts +++ b/libs/common/src/vault/models/domain/secure-note.ts @@ -1,17 +1,14 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Jsonify } from "type-fest"; import { SecureNote as SdkSecureNote } from "@bitwarden/sdk-internal"; import Domain from "../../../platform/models/domain/domain-base"; -import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { SecureNoteType } from "../../enums"; import { SecureNoteData } from "../data/secure-note.data"; import { SecureNoteView } from "../view/secure-note.view"; export class SecureNote extends Domain { - type: SecureNoteType; + type: SecureNoteType = SecureNoteType.Generic; constructor(obj?: SecureNoteData) { super(); @@ -22,11 +19,7 @@ export class SecureNote extends Domain { this.type = obj.type; } - async decrypt( - orgId: string, - context = "No Cipher Context", - encKey?: SymmetricCryptoKey, - ): Promise { + async decrypt(): Promise { return new SecureNoteView(this); } @@ -36,12 +29,14 @@ export class SecureNote extends Domain { return n; } - static fromJSON(obj: Jsonify): SecureNote { + static fromJSON(obj: Jsonify | undefined): SecureNote | undefined { if (obj == null) { - return null; + return undefined; } - return Object.assign(new SecureNote(), obj); + const secureNote = new SecureNote(); + secureNote.type = obj.type; + return secureNote; } /** @@ -59,7 +54,7 @@ export class SecureNote extends Domain { * Maps an SDK SecureNote object to a SecureNote * @param obj - The SDK SecureNote object */ - static fromSdkSecureNote(obj: SdkSecureNote): SecureNote | undefined { + static fromSdkSecureNote(obj?: SdkSecureNote): SecureNote | undefined { if (obj == null) { return undefined; } diff --git a/libs/common/src/vault/models/domain/ssh-key.spec.ts b/libs/common/src/vault/models/domain/ssh-key.spec.ts index 6576d1a41e9..38228e54a4a 100644 --- a/libs/common/src/vault/models/domain/ssh-key.spec.ts +++ b/libs/common/src/vault/models/domain/ssh-key.spec.ts @@ -1,3 +1,5 @@ +import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; + import { mockEnc } from "../../../../spec"; import { SshKeyApi } from "../api/ssh-key.api"; import { SshKeyData } from "../data/ssh-key.data"; @@ -31,11 +33,10 @@ describe("Sshkey", () => { const data = new SshKeyData(); const sshKey = new SshKey(data); - expect(sshKey).toEqual({ - privateKey: null, - publicKey: null, - keyFingerprint: null, - }); + expect(sshKey).toBeInstanceOf(SshKey); + expect(sshKey.privateKey).toBeInstanceOf(EncString); + expect(sshKey.publicKey).toBeInstanceOf(EncString); + expect(sshKey.keyFingerprint).toBeInstanceOf(EncString); }); it("toSshKeyData", () => { @@ -60,8 +61,8 @@ describe("Sshkey", () => { }); describe("fromJSON", () => { - it("returns null if object is null", () => { - expect(SshKey.fromJSON(null)).toBeNull(); + it("returns undefined if object is null", () => { + expect(SshKey.fromJSON(null)).toBeUndefined(); }); }); diff --git a/libs/common/src/vault/models/domain/ssh-key.ts b/libs/common/src/vault/models/domain/ssh-key.ts index ab1685955a3..a7028321a44 100644 --- a/libs/common/src/vault/models/domain/ssh-key.ts +++ b/libs/common/src/vault/models/domain/ssh-key.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Jsonify } from "type-fest"; import { SshKey as SdkSshKey } from "@bitwarden/sdk-internal"; @@ -11,9 +9,9 @@ import { SshKeyData } from "../data/ssh-key.data"; import { SshKeyView } from "../view/ssh-key.view"; export class SshKey extends Domain { - privateKey: EncString; - publicKey: EncString; - keyFingerprint: EncString; + privateKey!: EncString; + publicKey!: EncString; + keyFingerprint!: EncString; constructor(obj?: SshKeyData) { super(); @@ -21,20 +19,13 @@ export class SshKey extends Domain { return; } - this.buildDomainModel( - this, - obj, - { - privateKey: null, - publicKey: null, - keyFingerprint: null, - }, - [], - ); + this.privateKey = new EncString(obj.privateKey); + this.publicKey = new EncString(obj.publicKey); + this.keyFingerprint = new EncString(obj.keyFingerprint); } decrypt( - orgId: string, + orgId: string | undefined, context = "No Cipher Context", encKey?: SymmetricCryptoKey, ): Promise { @@ -42,7 +33,7 @@ export class SshKey extends Domain { this, new SshKeyView(), ["privateKey", "publicKey", "keyFingerprint"], - orgId, + orgId ?? null, encKey, "DomainType: SshKey; " + context, ); @@ -58,19 +49,17 @@ export class SshKey extends Domain { return c; } - static fromJSON(obj: Partial>): SshKey { + static fromJSON(obj: Jsonify | undefined): SshKey | undefined { if (obj == null) { - return null; + return undefined; } - const privateKey = EncString.fromJSON(obj.privateKey); - const publicKey = EncString.fromJSON(obj.publicKey); - const keyFingerprint = EncString.fromJSON(obj.keyFingerprint); - return Object.assign(new SshKey(), obj, { - privateKey, - publicKey, - keyFingerprint, - }); + const sshKey = new SshKey(); + sshKey.privateKey = EncString.fromJSON(obj.privateKey); + sshKey.publicKey = EncString.fromJSON(obj.publicKey); + sshKey.keyFingerprint = EncString.fromJSON(obj.keyFingerprint); + + return sshKey; } /** @@ -90,7 +79,7 @@ export class SshKey extends Domain { * Maps an SDK SshKey object to a SshKey * @param obj - The SDK SshKey object */ - static fromSdkSshKey(obj: SdkSshKey): SshKey | undefined { + static fromSdkSshKey(obj?: SdkSshKey): SshKey | undefined { if (obj == null) { return undefined; } diff --git a/libs/common/src/vault/models/request/cipher-partial.request.ts b/libs/common/src/vault/models/request/cipher-partial.request.ts index 6037dff6cb2..a50ea10d0cb 100644 --- a/libs/common/src/vault/models/request/cipher-partial.request.ts +++ b/libs/common/src/vault/models/request/cipher-partial.request.ts @@ -1,7 +1,7 @@ import { Cipher } from "../domain/cipher"; export class CipherPartialRequest { - folderId: string; + folderId?: string; favorite: boolean; constructor(cipher: Cipher) { diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 52c83c5a104..efe7bc2b89b 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -869,13 +869,14 @@ export class CipherService implements CipherServiceAbstraction { response = await this.apiService.postCipherAdmin(request); const data = new CipherData(response, cipher.collectionIds); return new Cipher(data); - } else if (cipher.collectionIds != null) { + } else if (cipher.collectionIds != null && cipher.collectionIds.length > 0) { const request = new CipherCreateRequest({ cipher, encryptedFor }); response = await this.apiService.postCipherCreate(request); } else { const request = new CipherRequest({ cipher, encryptedFor }); response = await this.apiService.postCipher(request); } + cipher.id = response.id; const data = new CipherData(response, cipher.collectionIds); diff --git a/libs/common/src/vault/utils/domain-utils.ts b/libs/common/src/vault/utils/domain-utils.ts new file mode 100644 index 00000000000..ee071b29ec3 --- /dev/null +++ b/libs/common/src/vault/utils/domain-utils.ts @@ -0,0 +1,27 @@ +import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; +import { EncString as SdkEncString } from "@bitwarden/sdk-internal"; + +/** + * Converts a string value to an EncString, handling null/undefined gracefully. + * + * @param value - The string value to convert, or undefined + * @returns An EncString instance if value is defined, otherwise undefined + * + */ +export const conditionalEncString = (value?: string): EncString | undefined => { + return value != null ? new EncString(value) : undefined; +}; + +/** + * Converts an EncString representation (from JSON or SDK) to a domain EncString instance. + * Handles both serialized JSON representations and SDK EncString objects. + * + * @param value - The EncString representation (string, object, or SdkEncString), or undefined + * @returns A domain EncString instance if value is defined, otherwise undefined + * + */ +export const encStringFrom = ( + value?: T, +): EncString | undefined => { + return value != null ? EncString.fromJSON(value) : undefined; +}; diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts index df317835392..4214873feed 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts @@ -343,7 +343,7 @@ describe("VaultExportService", () => { const exportData: BitwardenJsonExport = JSON.parse(data); expect(exportData.items.length).toBe(1); expect(exportData.items[0].id).toBe("mock-id"); - expect(exportData.items[0].organizationId).toBe(null); + expect(exportData.items[0].organizationId).toBeUndefined(); }); it.each([[400], [401], [404], [500]])( From bc0e0f0781454344859fa0d2e6e148c59da50100 Mon Sep 17 00:00:00 2001 From: Mick Letofsky Date: Fri, 24 Oct 2025 16:25:15 +0200 Subject: [PATCH 30/73] Update Claude owners (#17015) --- .github/CODEOWNERS | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f03cf3ee2a8..f784f375086 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -223,3 +223,8 @@ apps/web/src/locales/en/messages.json **/jest.config.js @bitwarden/team-platform-dev **/project.jsons @bitwarden/team-platform-dev libs/pricing @bitwarden/team-billing-dev + +# Claude related files +.claude/ @bitwarden/team-ai-sme +.github/workflows/respond.yml @bitwarden/team-ai-sme +.github/workflows/review-code.yml @bitwarden/team-ai-sme From 1da4fd22618fe87871fe1b4051b8680a2f5c49ff Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Fri, 24 Oct 2025 10:35:55 -0400 Subject: [PATCH 31/73] PM-26985 Use a Shadow DOM for the notification bar iframe to address FF fingerprinting issues (#16903) * PM-26985 Use a Shadow DOM for the notification bar iframe to address FF fingerprinting issues * update tests --- ...notifications-content.service.spec.ts.snap | 2 +- ...rlay-notifications-content.service.spec.ts | 40 ++++++++++--------- .../overlay-notifications-content.service.ts | 23 +++++++++-- 3 files changed, 42 insertions(+), 23 deletions(-) diff --git a/apps/browser/src/autofill/overlay/notifications/content/__snapshots__/overlay-notifications-content.service.spec.ts.snap b/apps/browser/src/autofill/overlay/notifications/content/__snapshots__/overlay-notifications-content.service.spec.ts.snap index e5bafe34b5f..39ca68d912c 100644 --- a/apps/browser/src/autofill/overlay/notifications/content/__snapshots__/overlay-notifications-content.service.spec.ts.snap +++ b/apps/browser/src/autofill/overlay/notifications/content/__snapshots__/overlay-notifications-content.service.spec.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`OverlayNotificationsContentService opening the notification bar creates the notification bar elements and appends them to the body 1`] = ` +exports[`OverlayNotificationsContentService opening the notification bar creates the notification bar elements and appends them to the body within a shadow root 1`] = `

{ let domElementVisibilityService: DomElementVisibilityService; let autofillInit: AutofillInit; let bodyAppendChildSpy: jest.SpyInstance; + let postMessageSpy: jest.SpyInstance>; beforeEach(() => { jest.useFakeTimers(); jest.spyOn(utils, "sendExtensionMessage").mockImplementation(jest.fn()); + jest.spyOn(HTMLIFrameElement.prototype, "contentWindow", "get").mockReturnValue(window); + postMessageSpy = jest.spyOn(window, "postMessage").mockImplementation(jest.fn()); domQueryService = mock(); domElementVisibilityService = new DomElementVisibilityService(); overlayNotificationsContentService = new OverlayNotificationsContentService(); @@ -48,7 +51,7 @@ describe("OverlayNotificationsContentService", () => { }); it("closes the notification bar if the notification bar type has changed", async () => { - overlayNotificationsContentService["currentNotificationBarType"] = "add"; + overlayNotificationsContentService["currentNotificationBarType"] = NotificationType.AddLogin; const closeNotificationBarSpy = jest.spyOn( overlayNotificationsContentService as any, "closeNotificationBar", @@ -66,7 +69,7 @@ describe("OverlayNotificationsContentService", () => { expect(closeNotificationBarSpy).toHaveBeenCalled(); }); - it("creates the notification bar elements and appends them to the body", async () => { + it("creates the notification bar elements and appends them to the body within a shadow root", async () => { sendMockExtensionMessage({ command: "openNotificationBar", data: { @@ -77,6 +80,13 @@ describe("OverlayNotificationsContentService", () => { await flushPromises(); expect(overlayNotificationsContentService["notificationBarElement"]).toMatchSnapshot(); + + const rootElement = overlayNotificationsContentService["notificationBarRootElement"]; + expect(bodyAppendChildSpy).toHaveBeenCalledWith(rootElement); + expect(rootElement?.tagName).toBe("BIT-NOTIFICATION-BAR-ROOT"); + + expect(document.getElementById("bit-notification-bar")).toBeNull(); + expect(document.querySelector("#bit-notification-bar-iframe")).toBeNull(); }); it("sets up a slide in animation when the notification is fresh", async () => { @@ -116,6 +126,8 @@ describe("OverlayNotificationsContentService", () => { }); it("sends an initialization message to the notification bar iframe", async () => { + const addEventListenerSpy = jest.spyOn(globalThis, "addEventListener"); + sendMockExtensionMessage({ command: "openNotificationBar", data: { @@ -124,10 +136,7 @@ describe("OverlayNotificationsContentService", () => { }, }); await flushPromises(); - const postMessageSpy = jest.spyOn( - overlayNotificationsContentService["notificationBarIframeElement"].contentWindow, - "postMessage", - ); + expect(addEventListenerSpy).toHaveBeenCalledWith("message", expect.any(Function)); globalThis.dispatchEvent( new MessageEvent("message", { @@ -142,7 +151,6 @@ describe("OverlayNotificationsContentService", () => { ); await flushPromises(); - expect(postMessageSpy).toHaveBeenCalledTimes(1); expect(postMessageSpy).toHaveBeenCalledWith( { command: "initNotificationBar", @@ -158,7 +166,7 @@ describe("OverlayNotificationsContentService", () => { sendMockExtensionMessage({ command: "openNotificationBar", data: { - type: "change", + type: NotificationType.ChangePassword, typeData: mock(), }, }); @@ -242,20 +250,15 @@ describe("OverlayNotificationsContentService", () => { }); it("sends a message to the notification bar iframe indicating that the save attempt completed", () => { - jest.spyOn( - overlayNotificationsContentService["notificationBarIframeElement"].contentWindow, - "postMessage", - ); - sendMockExtensionMessage({ command: "saveCipherAttemptCompleted", data: { error: undefined }, }); - expect( - overlayNotificationsContentService["notificationBarIframeElement"].contentWindow - .postMessage, - ).toHaveBeenCalledWith({ command: "saveCipherAttemptCompleted", error: undefined }, "*"); + expect(postMessageSpy).toHaveBeenCalledWith( + { command: "saveCipherAttemptCompleted", error: undefined }, + "*", + ); }); }); @@ -271,9 +274,10 @@ describe("OverlayNotificationsContentService", () => { await flushPromises(); }); - it("triggers a closure of the notification bar", () => { + it("triggers a closure of the notification bar and cleans up all shadow DOM elements", () => { overlayNotificationsContentService.destroy(); + expect(overlayNotificationsContentService["notificationBarRootElement"]).toBeNull(); expect(overlayNotificationsContentService["notificationBarElement"]).toBeNull(); expect(overlayNotificationsContentService["notificationBarIframeElement"]).toBeNull(); }); 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 4e09c3186bb..0afa4f1409b 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 @@ -17,8 +17,10 @@ import { export class OverlayNotificationsContentService implements OverlayNotificationsContentServiceInterface { + private notificationBarRootElement: HTMLElement | null = null; private notificationBarElement: HTMLElement | null = null; private notificationBarIframeElement: HTMLIFrameElement | null = null; + private notificationBarShadowRoot: ShadowRoot | null = null; private currentNotificationBarType: NotificationType | null = null; private notificationBarContainerStyles: Partial = { height: "400px", @@ -158,12 +160,12 @@ export class OverlayNotificationsContentService * @private */ private openNotificationBar(initData: NotificationBarIframeInitData) { - if (!this.notificationBarElement && !this.notificationBarIframeElement) { + if (!this.notificationBarRootElement && !this.notificationBarIframeElement) { this.createNotificationBarIframeElement(initData); this.createNotificationBarElement(); this.setupInitNotificationBarMessageListener(initData); - globalThis.document.body.appendChild(this.notificationBarElement); + globalThis.document.body.appendChild(this.notificationBarRootElement); } } @@ -213,15 +215,25 @@ export class OverlayNotificationsContentService }; /** - * Creates the container for the notification bar iframe. + * Creates the container for the notification bar iframe with shadow DOM. */ private createNotificationBarElement() { if (this.notificationBarIframeElement) { + this.notificationBarRootElement = globalThis.document.createElement( + "bit-notification-bar-root", + ); + + this.notificationBarShadowRoot = this.notificationBarRootElement.attachShadow({ + mode: "closed", + delegatesFocus: true, + }); + this.notificationBarElement = globalThis.document.createElement("div"); this.notificationBarElement.id = "bit-notification-bar"; setElementStyles(this.notificationBarElement, this.notificationBarContainerStyles, true); + this.notificationBarShadowRoot.appendChild(this.notificationBarElement); this.notificationBarElement.appendChild(this.notificationBarIframeElement); } } @@ -258,7 +270,7 @@ export class OverlayNotificationsContentService * @param closedByUserAction - Whether the notification bar was closed by the user. */ private closeNotificationBar(closedByUserAction: boolean = false) { - if (!this.notificationBarElement && !this.notificationBarIframeElement) { + if (!this.notificationBarRootElement && !this.notificationBarIframeElement) { return; } @@ -267,6 +279,9 @@ export class OverlayNotificationsContentService this.notificationBarElement.remove(); this.notificationBarElement = null; + this.notificationBarShadowRoot = null; + this.notificationBarRootElement.remove(); + this.notificationBarRootElement = null; const removableNotificationTypes = new Set([ NotificationTypes.Add, From fc26a21b85c6c61231c7efefb069208cb27635c0 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Fri, 24 Oct 2025 18:17:58 +0200 Subject: [PATCH 32/73] DIRT - Prefer signal & change detection (#16939) --- .../pages/phishing-warning.component.ts | 2 ++ .../pages/protected-by-component.ts | 2 ++ .../reports/pages/breach-report.component.ts | 2 ++ .../exposed-passwords-report.component.ts | 2 ++ .../inactive-two-factor-report.component.ts | 2 ++ .../exposed-passwords-report.component.ts | 2 ++ .../inactive-two-factor-report.component.ts | 2 ++ .../reused-passwords-report.component.ts | 2 ++ .../unsecured-websites-report.component.ts | 2 ++ .../weak-passwords-report.component.ts | 2 ++ .../reports/pages/reports-home.component.ts | 2 ++ .../reused-passwords-report.component.ts | 2 ++ .../unsecured-websites-report.component.ts | 2 ++ .../pages/weak-passwords-report.component.ts | 2 ++ .../dirt/reports/reports-layout.component.ts | 2 ++ .../report-card/report-card.component.ts | 12 ++++++++++ .../report-list/report-list.component.ts | 4 ++++ .../activity/activity-card.component.ts | 22 +++++++++++++++++++ .../activity/all-activity.component.ts | 2 ++ .../new-applications-dialog.component.ts | 2 ++ .../all-applications.component.ts | 2 ++ .../critical-applications.component.ts | 2 ++ .../risk-insights.component.ts | 2 ++ .../app-table-row-scrollable.component.ts | 18 +++++++++++++++ .../shared/risk-insights-loading.component.ts | 2 ++ .../integration-card.component.ts | 22 +++++++++++++++++++ .../connect-dialog-datadog.component.ts | 2 ++ .../connect-dialog-hec.component.ts | 2 ++ .../integration-grid.component.ts | 8 +++++++ .../integrations.component.ts | 2 ++ .../member-access-report.component.ts | 2 ++ libs/dirt/card/src/card.component.ts | 8 +++++++ 32 files changed, 144 insertions(+) diff --git a/apps/browser/src/dirt/phishing-detection/pages/phishing-warning.component.ts b/apps/browser/src/dirt/phishing-detection/pages/phishing-warning.component.ts index 4712c94c89e..6087042629a 100644 --- a/apps/browser/src/dirt/phishing-detection/pages/phishing-warning.component.ts +++ b/apps/browser/src/dirt/phishing-detection/pages/phishing-warning.component.ts @@ -21,6 +21,8 @@ import { import { PhishingDetectionService } from "../services/phishing-detection.service"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "dirt-phishing-warning", standalone: true, diff --git a/apps/browser/src/dirt/phishing-detection/pages/protected-by-component.ts b/apps/browser/src/dirt/phishing-detection/pages/protected-by-component.ts index 298c7acd38e..71cdac89aa2 100644 --- a/apps/browser/src/dirt/phishing-detection/pages/protected-by-component.ts +++ b/apps/browser/src/dirt/phishing-detection/pages/protected-by-component.ts @@ -6,6 +6,8 @@ import { Component } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { ButtonModule, LinkModule } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "dirt-phishing-protected-by", standalone: true, diff --git a/apps/web/src/app/dirt/reports/pages/breach-report.component.ts b/apps/web/src/app/dirt/reports/pages/breach-report.component.ts index b197c7dcae8..db85f503aec 100644 --- a/apps/web/src/app/dirt/reports/pages/breach-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/breach-report.component.ts @@ -8,6 +8,8 @@ import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BreachAccountResponse } from "@bitwarden/common/dirt/models/response/breach-account.response"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-breach-report", templateUrl: "breach-report.component.html", diff --git a/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.ts b/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.ts index bf2a528e723..51bdde3eda8 100644 --- a/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.ts @@ -18,6 +18,8 @@ import { CipherReportComponent } from "./cipher-report.component"; type ReportResult = CipherView & { exposedXTimes: number }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-exposed-passwords-report", templateUrl: "exposed-passwords-report.component.html", diff --git a/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.ts b/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.ts index 0024af35109..8b0fdda70e3 100644 --- a/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.ts @@ -19,6 +19,8 @@ import { AdminConsoleCipherFormConfigService } from "../../../vault/org-vault/se import { CipherReportComponent } from "./cipher-report.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-inactive-two-factor-report", templateUrl: "inactive-two-factor-report.component.html", diff --git a/apps/web/src/app/dirt/reports/pages/organizations/exposed-passwords-report.component.ts b/apps/web/src/app/dirt/reports/pages/organizations/exposed-passwords-report.component.ts index e7392ad609a..4dbd31ce4dc 100644 --- a/apps/web/src/app/dirt/reports/pages/organizations/exposed-passwords-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/organizations/exposed-passwords-report.component.ts @@ -24,6 +24,8 @@ import { RoutedVaultFilterService } from "../../../../vault/individual-vault/vau import { AdminConsoleCipherFormConfigService } from "../../../../vault/org-vault/services/admin-console-cipher-form-config.service"; import { ExposedPasswordsReportComponent as BaseExposedPasswordsReportComponent } from "../exposed-passwords-report.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-org-exposed-passwords-report", templateUrl: "../exposed-passwords-report.component.html", diff --git a/apps/web/src/app/dirt/reports/pages/organizations/inactive-two-factor-report.component.ts b/apps/web/src/app/dirt/reports/pages/organizations/inactive-two-factor-report.component.ts index 1105e814245..fde9c35a6de 100644 --- a/apps/web/src/app/dirt/reports/pages/organizations/inactive-two-factor-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/organizations/inactive-two-factor-report.component.ts @@ -23,6 +23,8 @@ import { RoutedVaultFilterService } from "../../../../vault/individual-vault/vau import { AdminConsoleCipherFormConfigService } from "../../../../vault/org-vault/services/admin-console-cipher-form-config.service"; import { InactiveTwoFactorReportComponent as BaseInactiveTwoFactorReportComponent } from "../inactive-two-factor-report.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-inactive-two-factor-report", templateUrl: "../inactive-two-factor-report.component.html", diff --git a/apps/web/src/app/dirt/reports/pages/organizations/reused-passwords-report.component.ts b/apps/web/src/app/dirt/reports/pages/organizations/reused-passwords-report.component.ts index 5c48919510e..5e457a91bd9 100644 --- a/apps/web/src/app/dirt/reports/pages/organizations/reused-passwords-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/organizations/reused-passwords-report.component.ts @@ -23,6 +23,8 @@ import { RoutedVaultFilterService } from "../../../../vault/individual-vault/vau import { AdminConsoleCipherFormConfigService } from "../../../../vault/org-vault/services/admin-console-cipher-form-config.service"; import { ReusedPasswordsReportComponent as BaseReusedPasswordsReportComponent } from "../reused-passwords-report.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-reused-passwords-report", templateUrl: "../reused-passwords-report.component.html", diff --git a/apps/web/src/app/dirt/reports/pages/organizations/unsecured-websites-report.component.ts b/apps/web/src/app/dirt/reports/pages/organizations/unsecured-websites-report.component.ts index dad9688f105..24f514d551f 100644 --- a/apps/web/src/app/dirt/reports/pages/organizations/unsecured-websites-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/organizations/unsecured-websites-report.component.ts @@ -23,6 +23,8 @@ import { RoutedVaultFilterService } from "../../../../vault/individual-vault/vau import { AdminConsoleCipherFormConfigService } from "../../../../vault/org-vault/services/admin-console-cipher-form-config.service"; import { UnsecuredWebsitesReportComponent as BaseUnsecuredWebsitesReportComponent } from "../unsecured-websites-report.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-unsecured-websites-report", templateUrl: "../unsecured-websites-report.component.html", diff --git a/apps/web/src/app/dirt/reports/pages/organizations/weak-passwords-report.component.ts b/apps/web/src/app/dirt/reports/pages/organizations/weak-passwords-report.component.ts index 67ca5081b6b..50c18d1da3b 100644 --- a/apps/web/src/app/dirt/reports/pages/organizations/weak-passwords-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/organizations/weak-passwords-report.component.ts @@ -24,6 +24,8 @@ import { RoutedVaultFilterService } from "../../../../vault/individual-vault/vau import { AdminConsoleCipherFormConfigService } from "../../../../vault/org-vault/services/admin-console-cipher-form-config.service"; import { WeakPasswordsReportComponent as BaseWeakPasswordsReportComponent } from "../weak-passwords-report.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-weak-passwords-report", templateUrl: "../weak-passwords-report.component.html", diff --git a/apps/web/src/app/dirt/reports/pages/reports-home.component.ts b/apps/web/src/app/dirt/reports/pages/reports-home.component.ts index acc3efac58a..a0e3a73aa3f 100644 --- a/apps/web/src/app/dirt/reports/pages/reports-home.component.ts +++ b/apps/web/src/app/dirt/reports/pages/reports-home.component.ts @@ -9,6 +9,8 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs import { reports, ReportType } from "../reports"; import { ReportEntry, ReportVariant } from "../shared"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-reports-home", templateUrl: "reports-home.component.html", diff --git a/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.ts b/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.ts index 8e1e4fcf0cc..0a81b19d4ff 100644 --- a/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/reused-passwords-report.component.ts @@ -17,6 +17,8 @@ import { AdminConsoleCipherFormConfigService } from "../../../vault/org-vault/se import { CipherReportComponent } from "./cipher-report.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-reused-passwords-report", templateUrl: "reused-passwords-report.component.html", diff --git a/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.ts b/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.ts index 4b9cc3fd789..4a2c0677574 100644 --- a/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/unsecured-websites-report.component.ts @@ -16,6 +16,8 @@ import { AdminConsoleCipherFormConfigService } from "../../../vault/org-vault/se import { CipherReportComponent } from "./cipher-report.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-unsecured-websites-report", templateUrl: "unsecured-websites-report.component.html", diff --git a/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.ts b/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.ts index 0472dbfaa6f..bb5400346fd 100644 --- a/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/weak-passwords-report.component.ts @@ -22,6 +22,8 @@ import { CipherReportComponent } from "./cipher-report.component"; type ReportScore = { label: string; badgeVariant: BadgeVariant; sortOrder: number }; type ReportResult = CipherView & { score: number; reportValue: ReportScore; scoreKey: number }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-weak-passwords-report", templateUrl: "weak-passwords-report.component.html", diff --git a/apps/web/src/app/dirt/reports/reports-layout.component.ts b/apps/web/src/app/dirt/reports/reports-layout.component.ts index 360898e6057..c2fbf858590 100644 --- a/apps/web/src/app/dirt/reports/reports-layout.component.ts +++ b/apps/web/src/app/dirt/reports/reports-layout.component.ts @@ -3,6 +3,8 @@ import { NavigationEnd, Router } from "@angular/router"; import { Subscription } from "rxjs"; import { filter } from "rxjs/operators"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-reports-layout", templateUrl: "reports-layout.component.html", diff --git a/apps/web/src/app/dirt/reports/shared/report-card/report-card.component.ts b/apps/web/src/app/dirt/reports/shared/report-card/report-card.component.ts index e8ffcd01068..565035c2c55 100644 --- a/apps/web/src/app/dirt/reports/shared/report-card/report-card.component.ts +++ b/apps/web/src/app/dirt/reports/shared/report-card/report-card.component.ts @@ -6,16 +6,28 @@ import { Icon } from "@bitwarden/assets/svg"; import { ReportVariant } from "../models/report-variant"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-report-card", templateUrl: "report-card.component.html", standalone: false, }) export class ReportCardComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() title: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() description: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() route: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() icon: Icon; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() variant: ReportVariant; protected get disabled() { diff --git a/apps/web/src/app/dirt/reports/shared/report-list/report-list.component.ts b/apps/web/src/app/dirt/reports/shared/report-list/report-list.component.ts index c81c99d50d5..509e2f3b872 100644 --- a/apps/web/src/app/dirt/reports/shared/report-list/report-list.component.ts +++ b/apps/web/src/app/dirt/reports/shared/report-list/report-list.component.ts @@ -4,11 +4,15 @@ import { Component, Input } from "@angular/core"; import { ReportEntry } from "../models/report-entry"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-report-list", templateUrl: "report-list.component.html", standalone: false, }) export class ReportListComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() reports: ReportEntry[]; } diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.ts index c8c73cd0e5a..7abedb06a7c 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.ts @@ -5,6 +5,8 @@ import { Router } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { ButtonModule, ButtonType, LinkModule, TypographyModule } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "dirt-activity-card", templateUrl: "./activity-card.component.html", @@ -18,50 +20,70 @@ export class ActivityCardComponent { /** * The title of the card goes here */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() title: string = ""; /** * The card metrics text to display next to the value */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() cardMetrics: string = ""; /** * The description text to display below the value and metrics */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() metricDescription: string = ""; /** * The link to navigate to for more information */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() navigationLink: string = ""; /** * The text to display for the navigation link */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() navigationText: string = ""; /** * Show Navigation link */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() showNavigationLink: boolean = false; /** * Icon class to display next to metrics (e.g., "bwi-exclamation-triangle"). * If null, no icon is displayed. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() iconClass: string | null = null; /** * Button text. If provided, a button will be displayed instead of a navigation link. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() buttonText: string = ""; /** * Button type (e.g., "primary", "secondary") */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() buttonType: ButtonType = "primary"; /** * Event emitted when button is clicked */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() buttonClick = new EventEmitter(); constructor(private router: Router) {} diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/all-activity.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/all-activity.component.ts index 9e3dff3144c..947e2f2fa42 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/all-activity.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/all-activity.component.ts @@ -22,6 +22,8 @@ import { ActivityCardComponent } from "./activity-card.component"; import { PasswordChangeMetricComponent } from "./activity-cards/password-change-metric.component"; import { NewApplicationsDialogComponent } from "./new-applications-dialog.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "dirt-all-activity", imports: [ diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/new-applications-dialog.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/new-applications-dialog.component.ts index e06d889c59e..05b47da40ed 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/new-applications-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/new-applications-dialog.component.ts @@ -15,6 +15,8 @@ export interface NewApplicationsDialogData { newApplications: string[]; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "./new-applications-dialog.component.html", imports: [CommonModule, ButtonModule, DialogModule, TypographyModule, I18nPipe], diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/all-applications.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/all-applications.component.ts index 57ee0b20360..5fbc841778a 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/all-applications.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications/all-applications.component.ts @@ -28,6 +28,8 @@ import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pip import { AppTableRowScrollableComponent } from "../shared/app-table-row-scrollable.component"; import { ApplicationsLoadingComponent } from "../shared/risk-insights-loading.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "dirt-all-applications", templateUrl: "./all-applications.component.html", diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications/critical-applications.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications/critical-applications.component.ts index dffc493e51d..e297f8eda3c 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications/critical-applications.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications/critical-applications.component.ts @@ -28,6 +28,8 @@ import { RiskInsightsTabType } from "../models/risk-insights.models"; import { AppTableRowScrollableComponent } from "../shared/app-table-row-scrollable.component"; import { AccessIntelligenceSecurityTasksService } from "../shared/security-tasks.service"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "dirt-critical-applications", templateUrl: "./critical-applications.component.html", diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts index e1264b009b8..8e58ba22454 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts @@ -28,6 +28,8 @@ import { AllApplicationsComponent } from "./all-applications/all-applications.co import { CriticalApplicationsComponent } from "./critical-applications/critical-applications.component"; import { RiskInsightsTabType } from "./models/risk-insights.models"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "./risk-insights.component.html", imports: [ diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable.component.ts index e34b13176ee..f2ecff75847 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/app-table-row-scrollable.component.ts @@ -7,19 +7,37 @@ import { MenuModule, TableDataSource, TableModule } from "@bitwarden/components" import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-table-row-scrollable", imports: [CommonModule, JslibModule, TableModule, SharedModule, PipesModule, MenuModule], templateUrl: "./app-table-row-scrollable.component.html", }) export class AppTableRowScrollableComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() dataSource!: TableDataSource; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() showRowMenuForCriticalApps: boolean = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() showRowCheckBox: boolean = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() selectedUrls: Set = new Set(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() openApplication: string = ""; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() showAppAtRiskMembers!: (applicationName: string) => void; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() unmarkAsCritical!: (applicationName: string) => void; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() checkboxChange!: (applicationName: string, $event: Event) => void; } diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/risk-insights-loading.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/risk-insights-loading.component.ts index 1d18ca3a030..d9cd8878b75 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/risk-insights-loading.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/risk-insights-loading.component.ts @@ -3,6 +3,8 @@ import { Component } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "dirt-risk-insights-loading", imports: [CommonModule, JslibModule], diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts index 3a243f8eb91..f1b0f982d57 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts @@ -32,6 +32,8 @@ import { openHecConnectDialog, } from "../integration-dialog/index"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-integration-card", templateUrl: "./integration-card.component.html", @@ -39,15 +41,29 @@ import { }) export class IntegrationCardComponent implements AfterViewInit, OnDestroy { private destroyed$: Subject = new Subject(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("imageEle") imageEle!: ElementRef; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() name: string = ""; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() image: string = ""; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() imageDarkMode: string = ""; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() linkURL: string = ""; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() integrationSettings!: Integration; /** Adds relevant `rel` attribute to external links */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() externalURL?: boolean; /** @@ -56,8 +72,14 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { * * @example "2024-12-31" */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() newBadgeExpiration?: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() description?: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() canSetupConnection?: boolean; organizationId: OrganizationId; diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-datadog.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-datadog.component.ts index d186910d2f7..47760c6311a 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-datadog.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-datadog.component.ts @@ -27,6 +27,8 @@ export const DatadogConnectDialogResultStatus = { export type DatadogConnectDialogResultStatusType = (typeof DatadogConnectDialogResultStatus)[keyof typeof DatadogConnectDialogResultStatus]; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "./connect-dialog-datadog.component.html", imports: [SharedModule], diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.ts index dc3490843cf..3612f2c76cb 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.ts @@ -28,6 +28,8 @@ export const HecConnectDialogResultStatus = { export type HecConnectDialogResultStatusType = (typeof HecConnectDialogResultStatus)[keyof typeof HecConnectDialogResultStatus]; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "./connect-dialog-hec.component.html", imports: [SharedModule], diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-grid/integration-grid.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-grid/integration-grid.component.ts index 66ccc2530c2..19f15d1caea 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-grid/integration-grid.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-grid/integration-grid.component.ts @@ -6,15 +6,23 @@ import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { IntegrationCardComponent } from "../integration-card/integration-card.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-integration-grid", templateUrl: "./integration-grid.component.html", imports: [IntegrationCardComponent, SharedModule], }) export class IntegrationGridComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() integrations: Integration[] = []; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() ariaI18nKey: string = "integrationCardAriaLabel"; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() tooltipI18nKey: string = "integrationCardTooltip"; protected IntegrationType = IntegrationType; diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.ts index f0292ef90e7..f19fa6178bf 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.ts @@ -21,6 +21,8 @@ import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { IntegrationGridComponent } from "./integration-grid/integration-grid.component"; import { FilterIntegrationsPipe } from "./integrations.pipe"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "ac-integrations", templateUrl: "./integrations.component.html", diff --git a/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/member-access-report.component.ts b/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/member-access-report.component.ts index 796cf212a67..ad15edd84df 100644 --- a/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/member-access-report.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/member-access-report.component.ts @@ -31,6 +31,8 @@ import { MemberAccessReportService } from "./services/member-access-report.servi import { userReportItemHeaders } from "./view/member-access-export.view"; import { MemberAccessReportView } from "./view/member-access-report.view"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "member-access-report", templateUrl: "member-access-report.component.html", diff --git a/libs/dirt/card/src/card.component.ts b/libs/dirt/card/src/card.component.ts index f9899125dbd..b9f2e7aa72e 100644 --- a/libs/dirt/card/src/card.component.ts +++ b/libs/dirt/card/src/card.component.ts @@ -6,6 +6,8 @@ import { Component, Input } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { TypographyModule } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "dirt-card", templateUrl: "./card.component.html", @@ -19,13 +21,19 @@ export class CardComponent { /** * The title of the card */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() title: string; /** * The current value of the card as emphasized text */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() value: number; /** * The maximum value of the card */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() maxValue: number; } From f5f9d1881ea5c9b9eee9621ad489574c6c2013eb Mon Sep 17 00:00:00 2001 From: Alex <55413326+AlexRubik@users.noreply.github.com> Date: Fri, 24 Oct 2025 12:20:40 -0400 Subject: [PATCH 33/73] [PM-27291] preserve critical app flags when generating new reports (#17008) --- .../domain/risk-insights-orchestrator.service.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-orchestrator.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-orchestrator.service.ts index b9df2748e85..f52ab68985b 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-orchestrator.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-orchestrator.service.ts @@ -101,6 +101,8 @@ export class RiskInsightsOrchestratorService { // --------------------------- Trigger subjects --------------------- private _initializeOrganizationTriggerSubject = new Subject(); private _fetchReportTriggerSubject = new Subject(); + private _markUnmarkUpdatesSubject = new Subject(); + private _markUnmarkUpdates$ = this._markUnmarkUpdatesSubject.asObservable(); private _reportStateSubscription: Subscription | null = null; private _migrationSubscription: Subscription | null = null; @@ -236,7 +238,9 @@ export class RiskInsightsOrchestratorService { ) .pipe( map(() => updatedState), - tap((finalState) => this._rawReportDataSubject.next(finalState)), + tap((finalState) => { + this._markUnmarkUpdatesSubject.next(finalState); + }), catchError((error: unknown) => { this.logService.error("Failed to save updated applicationData", error); return of({ ...reportState, error: "Failed to remove a critical application" }); @@ -318,7 +322,9 @@ export class RiskInsightsOrchestratorService { ) .pipe( map(() => updatedState), - tap((finalState) => this._rawReportDataSubject.next(finalState)), + tap((finalState) => { + this._markUnmarkUpdatesSubject.next(finalState); + }), catchError((error: unknown) => { this.logService.error("Failed to save updated applicationData", error); return of({ ...reportState, error: "Failed to save critical applications" }); @@ -402,10 +408,10 @@ export class RiskInsightsOrchestratorService { }, }; }), - catchError(() => { + catchError((): Observable => { return of({ loading: false, error: "Failed to generate or save report", data: null }); }), - startWith({ loading: true, error: null, data: null }), + startWith({ loading: true, error: null, data: null }), ); } @@ -714,6 +720,7 @@ export class RiskInsightsOrchestratorService { initialReportLoad$, manualReportFetch$, newReportGeneration$, + this._markUnmarkUpdates$, ).pipe( scan((prevState: ReportState, currState: ReportState) => ({ ...prevState, From b26be1eec6caaf77698c20bd5cda1bbaa40822ca Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Fri, 24 Oct 2025 09:36:16 -0700 Subject: [PATCH 34/73] [PM-27059] Browser: Retain vault filters when editing a cipher from the dropdown (#16910) * Skip clearing vault filters if a cipher is being edited * add unit tests for clearVaultStateGuard --- .../guards/clear-vault-state.guard.spec.ts | 77 +++++++++++++++++++ .../popup/guards/clear-vault-state.guard.ts | 11 ++- 2 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 apps/browser/src/vault/popup/guards/clear-vault-state.guard.spec.ts diff --git a/apps/browser/src/vault/popup/guards/clear-vault-state.guard.spec.ts b/apps/browser/src/vault/popup/guards/clear-vault-state.guard.spec.ts new file mode 100644 index 00000000000..7ead8576b37 --- /dev/null +++ b/apps/browser/src/vault/popup/guards/clear-vault-state.guard.spec.ts @@ -0,0 +1,77 @@ +import { TestBed } from "@angular/core/testing"; +import { RouterStateSnapshot } from "@angular/router"; + +import { VaultV2Component } from "../components/vault-v2/vault-v2.component"; +import { VaultPopupItemsService } from "../services/vault-popup-items.service"; +import { VaultPopupListFiltersService } from "../services/vault-popup-list-filters.service"; + +import { clearVaultStateGuard } from "./clear-vault-state.guard"; + +describe("clearVaultStateGuard", () => { + let applyFilterSpy: jest.Mock; + let resetFilterFormSpy: jest.Mock; + + beforeEach(() => { + applyFilterSpy = jest.fn(); + resetFilterFormSpy = jest.fn(); + + TestBed.configureTestingModule({ + providers: [ + { + provide: VaultPopupItemsService, + useValue: { applyFilter: applyFilterSpy }, + }, + { + provide: VaultPopupListFiltersService, + useValue: { resetFilterForm: resetFilterFormSpy }, + }, + ], + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it.each([ + "/view-cipher?cipherId=123", + "/edit-cipher?cipherId=123", + "/clone-cipher?cipherId=123", + "/assign-collections?cipherId=123", + ])("should not clear vault state when viewing or editing a cipher: %s", (url) => { + const nextState = { url } as RouterStateSnapshot; + + const result = TestBed.runInInjectionContext(() => + clearVaultStateGuard({} as VaultV2Component, null, null, nextState), + ); + + expect(result).toBe(true); + expect(applyFilterSpy).not.toHaveBeenCalled(); + expect(resetFilterFormSpy).not.toHaveBeenCalled(); + }); + + it.each(["/settings", "/tabs/settings"])( + "should clear vault state when navigating to non-cipher routes: %s", + (url) => { + const nextState = { url } as RouterStateSnapshot; + + const result = TestBed.runInInjectionContext(() => + clearVaultStateGuard({} as VaultV2Component, null, null, nextState), + ); + + expect(result).toBe(true); + expect(applyFilterSpy).toHaveBeenCalledWith(""); + expect(resetFilterFormSpy).toHaveBeenCalled(); + }, + ); + + it("should not clear vault state when not changing states", () => { + const result = TestBed.runInInjectionContext(() => + clearVaultStateGuard({} as VaultV2Component, null, null, null), + ); + + expect(result).toBe(true); + expect(applyFilterSpy).not.toHaveBeenCalled(); + expect(resetFilterFormSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/browser/src/vault/popup/guards/clear-vault-state.guard.ts b/apps/browser/src/vault/popup/guards/clear-vault-state.guard.ts index e27090180d6..2a87db6e903 100644 --- a/apps/browser/src/vault/popup/guards/clear-vault-state.guard.ts +++ b/apps/browser/src/vault/popup/guards/clear-vault-state.guard.ts @@ -7,7 +7,8 @@ import { VaultPopupListFiltersService } from "../services/vault-popup-list-filte /** * Guard to clear the vault state (search and filter) when navigating away from the vault view. - * This ensures the search and filter state is reset when navigating between different tabs, except viewing a cipher. + * This ensures the search and filter state is reset when navigating between different tabs, + * except viewing or editing a cipher. */ export const clearVaultStateGuard: CanDeactivateFn = ( component: VaultV2Component, @@ -17,7 +18,7 @@ export const clearVaultStateGuard: CanDeactivateFn = ( ) => { const vaultPopupItemsService = inject(VaultPopupItemsService); const vaultPopupListFiltersService = inject(VaultPopupListFiltersService); - if (nextState && !isViewingCipher(nextState.url)) { + if (nextState && !isCipherOpen(nextState.url)) { vaultPopupItemsService.applyFilter(""); vaultPopupListFiltersService.resetFilterForm(); } @@ -25,4 +26,8 @@ export const clearVaultStateGuard: CanDeactivateFn = ( return true; }; -const isViewingCipher = (url: string): boolean => url.includes("view-cipher"); +const isCipherOpen = (url: string): boolean => + url.includes("view-cipher") || + url.includes("assign-collections") || + url.includes("edit-cipher") || + url.includes("clone-cipher"); From bcc92387b25481ef7b1b39d2e6082cdaff05cd13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Fri, 24 Oct 2025 20:42:18 +0100 Subject: [PATCH 35/73] [PM-26294] Re-implement SSO and TDE checks for device approvals access after provider user fix (#16642) --- .../models/domain/organization.spec.ts | 22 +++++++++++++++++++ .../models/domain/organization.ts | 7 +++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/libs/common/src/admin-console/models/domain/organization.spec.ts b/libs/common/src/admin-console/models/domain/organization.spec.ts index ddf1010eea9..cc158c71056 100644 --- a/libs/common/src/admin-console/models/domain/organization.spec.ts +++ b/libs/common/src/admin-console/models/domain/organization.spec.ts @@ -111,6 +111,28 @@ describe("Organization", () => { expect(organization.canManageDeviceApprovals).toBe(false); }); + it("should return false when ssoEnabled is false", () => { + data.type = OrganizationUserType.Admin; + data.useSso = true; + data.ssoEnabled = false; + data.ssoMemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption; + + const organization = new Organization(data); + + expect(organization.canManageDeviceApprovals).toBe(false); + }); + + it("should return false when ssoMemberDecryptionType is not TrustedDeviceEncryption", () => { + data.type = OrganizationUserType.Admin; + data.useSso = true; + data.ssoEnabled = true; + data.ssoMemberDecryptionType = MemberDecryptionType.MasterPassword; + + const organization = new Organization(data); + + expect(organization.canManageDeviceApprovals).toBe(false); + }); + it("should return true when admin has all required SSO settings enabled", () => { data.type = OrganizationUserType.Admin; data.useSso = true; diff --git a/libs/common/src/admin-console/models/domain/organization.ts b/libs/common/src/admin-console/models/domain/organization.ts index aea796dfc39..f320a675b62 100644 --- a/libs/common/src/admin-console/models/domain/organization.ts +++ b/libs/common/src/admin-console/models/domain/organization.ts @@ -311,7 +311,12 @@ export class Organization { } get canManageDeviceApprovals() { - return (this.isAdmin || this.permissions.manageResetPassword) && this.useSso; + return ( + (this.isAdmin || this.permissions.manageResetPassword) && + this.useSso && + this.ssoEnabled && + this.ssoMemberDecryptionType === MemberDecryptionType.TrustedDeviceEncryption + ); } get isExemptFromPolicies() { From e8db35907dc1a077c422b9feae661a8bfcb2068d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 10:59:16 +0100 Subject: [PATCH 36/73] [deps] Platform: Update Rust crate windows-registry to v0.6.1 (#16419) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- apps/desktop/desktop_native/Cargo.lock | 50 +++++++++++++++++++------- apps/desktop/desktop_native/Cargo.toml | 2 +- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 5dec59f0f12..5e658546671 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -3982,7 +3982,7 @@ dependencies = [ "windows-collections", "windows-core 0.61.0", "windows-future", - "windows-link", + "windows-link 0.1.3", "windows-numerics", ] @@ -4015,9 +4015,9 @@ checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ "windows-implement 0.60.0", "windows-interface 0.59.1", - "windows-link", + "windows-link 0.1.3", "windows-result 0.3.4", - "windows-strings", + "windows-strings 0.4.2", ] [[package]] @@ -4027,7 +4027,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32" dependencies = [ "windows-core 0.61.0", - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -4080,6 +4080,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-numerics" version = "0.2.0" @@ -4087,18 +4093,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ "windows-core 0.61.0", - "windows-link", + "windows-link 0.1.3", ] [[package]] name = "windows-registry" -version = "0.5.3" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "windows-link", - "windows-result 0.3.4", - "windows-strings", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] @@ -4116,7 +4122,16 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", ] [[package]] @@ -4125,7 +4140,16 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", ] [[package]] @@ -4216,7 +4240,7 @@ version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ - "windows-link", + "windows-link 0.1.3", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index edf3cb44eca..c0fe0b46f58 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -75,7 +75,7 @@ widestring = "=1.2.0" windows = "=0.61.1" windows-core = "=0.61.0" windows-future = "=0.2.0" -windows-registry = "=0.5.3" +windows-registry = "=0.6.1" zbus = "=5.11.0" zbus_polkit = "=5.0.0" zeroizing-alloc = "=0.1.0" From c8ddaae6b34d1a450dd54678c1fc5300d3709c13 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 27 Oct 2025 13:11:29 +0100 Subject: [PATCH 37/73] [PM-27300] Update SDK to 357 (#17003) * Update sdk to 357 * Package.lock --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index c9abe11b585..8ce60e0826f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "@angular/platform-browser": "19.2.14", "@angular/platform-browser-dynamic": "19.2.14", "@angular/router": "19.2.14", - "@bitwarden/sdk-internal": "0.2.0-main.315", + "@bitwarden/sdk-internal": "0.2.0-main.357", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", @@ -4690,9 +4690,9 @@ "link": true }, "node_modules/@bitwarden/sdk-internal": { - "version": "0.2.0-main.315", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.315.tgz", - "integrity": "sha512-hdpFRLrDYSJ6+cNXfMyHdTgg/xIePIlEUSn4JWzwru4PvTcEkkFwGJM3L2LoUqTdNMiDQlr0UjDahopT+C2r0g==", + "version": "0.2.0-main.357", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.357.tgz", + "integrity": "sha512-qo8kCzrWNJP69HeI6WRyJMCFXYUJqLbaQCFoDgQkQa3ICrwpw5g9gW5y4P9FOa/DHdj8BgVbFGAX+YylbUb0/A==", "license": "GPL-3.0", "dependencies": { "type-fest": "^4.41.0" diff --git a/package.json b/package.json index 88cf2bda43c..89e127488b2 100644 --- a/package.json +++ b/package.json @@ -159,7 +159,7 @@ "@angular/platform-browser": "19.2.14", "@angular/platform-browser-dynamic": "19.2.14", "@angular/router": "19.2.14", - "@bitwarden/sdk-internal": "0.2.0-main.315", + "@bitwarden/sdk-internal": "0.2.0-main.357", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", From a6882c36b94b1a439a925fbd176582c70d1730fa Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Mon, 27 Oct 2025 13:18:08 +0100 Subject: [PATCH 38/73] Resolve the redirect to subscription (#17017) --- .../organization-subscription-cloud.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html index 5fa10c4c87c..db3dde217c7 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html @@ -223,7 +223,7 @@

{{ "manageSubscription" | i18n }}

{{ "manageSubscriptionFromThe" | i18n }} - {{ + {{ "providerPortal" | i18n }}. From 43a1dfa46327d06d17eb4a23e030375b586255d0 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Mon, 27 Oct 2025 13:40:56 +0100 Subject: [PATCH 39/73] icons and key connector urls for web development (#17043) --- apps/web/config/development.json | 1 + apps/web/config/selfhosted.json | 1 + apps/web/webpack.base.js | 7 +++++++ 3 files changed, 9 insertions(+) diff --git a/apps/web/config/development.json b/apps/web/config/development.json index 52a0fb0fdf2..6fd5fa49eb2 100644 --- a/apps/web/config/development.json +++ b/apps/web/config/development.json @@ -8,6 +8,7 @@ "proxyIdentity": "http://localhost:33656", "proxyEvents": "http://localhost:46273", "proxyNotifications": "http://localhost:61840", + "proxyIcons": "http://localhost:50024", "wsConnectSrc": "ws://localhost:61840" }, "additionalRegions": [ diff --git a/apps/web/config/selfhosted.json b/apps/web/config/selfhosted.json index cd36ab15c5e..ffb7621e594 100644 --- a/apps/web/config/selfhosted.json +++ b/apps/web/config/selfhosted.json @@ -4,6 +4,7 @@ "proxyIdentity": "http://localhost:33657", "proxyEvents": "http://localhost:46274", "proxyNotifications": "http://localhost:61841", + "proxyKeyConnector": "http://localhost:5000", "port": 8081 }, "flags": {} diff --git a/apps/web/webpack.base.js b/apps/web/webpack.base.js index 7930a55f61a..56fd6c7faf5 100644 --- a/apps/web/webpack.base.js +++ b/apps/web/webpack.base.js @@ -276,6 +276,13 @@ module.exports.buildConfig = function buildConfig(params) { secure: false, changeOrigin: true, }, + { + context: ["/key-connector"], + target: envConfig.dev?.proxyKeyConnector, + pathRewrite: { "^/key-connector": "" }, + secure: false, + changeOrigin: true, + }, ], headers: (req) => { if (!req.originalUrl.includes("connector.html")) { From b9f48d83b21734540340444c3c971eb3b5a7956c Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Mon, 27 Oct 2025 13:53:05 +0100 Subject: [PATCH 40/73] [PM 25897] Copy and UI Tweaks for Payment Method Component (#16851) * Implement the Ui changes to align as expected * Align the Text in card number, expiration date and security code vertically * Change the Zip to ZIP * Remove readonly modifier from signal declarations --- apps/browser/src/_locales/en/messages.json | 6 ++++++ apps/desktop/src/locales/en/messages.json | 6 ++++++ .../payment/components/enter-billing-address.component.ts | 2 +- .../payment/components/enter-payment-method.component.ts | 6 +++--- .../billing/payment/components/payment-label.component.ts | 2 +- apps/web/src/app/billing/services/stripe.service.ts | 2 ++ apps/web/src/locales/en/messages.json | 6 ++++++ libs/common/src/vault/models/view/identity.view.ts | 2 +- .../cipher-form/components/identity/identity.component.html | 2 +- 9 files changed, 27 insertions(+), 7 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 6a0e8c01c4d..29601bfa70c 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -5721,5 +5721,11 @@ "settingDisabledByPolicy": { "message": "This setting is disabled by your organization's policy.", "description": "This hint text is displayed when a user setting is disabled due to an organization policy." + }, + "zipPostalCodeLabel": { + "message": "ZIP / Postal code" + }, + "cardNumberLabel": { + "message": "Card number" } } diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 3e004e270a3..32545a0c1cd 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -4181,5 +4181,11 @@ }, "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" + }, + "zipPostalCodeLabel": { + "message": "ZIP / Postal code" + }, + "cardNumberLabel": { + "message": "Card number" } } diff --git a/apps/web/src/app/billing/payment/components/enter-billing-address.component.ts b/apps/web/src/app/billing/payment/components/enter-billing-address.component.ts index 40785e9b7ea..db95beea7f8 100644 --- a/apps/web/src/app/billing/payment/components/enter-billing-address.component.ts +++ b/apps/web/src/app/billing/payment/components/enter-billing-address.component.ts @@ -70,7 +70,7 @@ type Scenario =

- {{ "zipPostalCode" | i18n }} + {{ "zipPostalCodeLabel" | i18n }}
- {{ "number" | i18n }} + {{ "cardNumberLabel" | i18n }}
@@ -109,7 +109,7 @@ type PaymentMethodFormGroup = FormGroup<{ class="tw-border-none tw-bg-transparent tw-text-primary-600 tw-pr-1" [position]="'above-end'" > - +

{{ "cardSecurityCodeDescription" | i18n }}

@@ -217,7 +217,7 @@ type PaymentMethodFormGroup = FormGroup<{
- {{ "zipPostalCode" | i18n }} + {{ "zipPostalCodeLabel" | i18n }} - ({{ "required" | i18n }}) + ({{ "required" | i18n }})
`, diff --git a/apps/web/src/app/billing/services/stripe.service.ts b/apps/web/src/app/billing/services/stripe.service.ts index 7ea0d7d52c8..f7655ba0c6e 100644 --- a/apps/web/src/app/billing/services/stripe.service.ts +++ b/apps/web/src/app/billing/services/stripe.service.ts @@ -230,6 +230,8 @@ export class StripeService { '"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"', fontSize: "16px", fontSmoothing: "antialiased", + lineHeight: "1.5", + padding: "8px 12px", "::placeholder": { color: null, }, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index f2cb9e5fd7b..f88af8aa1a7 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -11939,5 +11939,11 @@ }, "encryptionKeySettingsAlgorithmPopoverArgon2Id": { "message": "Argon2id offers stronger protection against modern attacks. Best for advanced users with powerful devices." + }, + "zipPostalCodeLabel": { + "message": "ZIP / Postal code" + }, + "cardNumberLabel": { + "message": "Card number" } } diff --git a/libs/common/src/vault/models/view/identity.view.ts b/libs/common/src/vault/models/view/identity.view.ts index 5fb0d1acba5..dca54fa04e8 100644 --- a/libs/common/src/vault/models/view/identity.view.ts +++ b/libs/common/src/vault/models/view/identity.view.ts @@ -23,7 +23,7 @@ export class IdentityView extends ItemView implements SdkIdentityView { city: string | undefined; @linkedFieldOption(LinkedId.State, { sortPosition: 16, i18nKey: "stateProvince" }) state: string | undefined; - @linkedFieldOption(LinkedId.PostalCode, { sortPosition: 17, i18nKey: "zipPostalCode" }) + @linkedFieldOption(LinkedId.PostalCode, { sortPosition: 17, i18nKey: "zipPostalCodeLabel" }) postalCode: string | undefined; @linkedFieldOption(LinkedId.Country, { sortPosition: 18 }) country: string | undefined; diff --git a/libs/vault/src/cipher-form/components/identity/identity.component.html b/libs/vault/src/cipher-form/components/identity/identity.component.html index 7f49bc21a10..2489977f63f 100644 --- a/libs/vault/src/cipher-form/components/identity/identity.component.html +++ b/libs/vault/src/cipher-form/components/identity/identity.component.html @@ -144,7 +144,7 @@ - {{ "zipPostalCode" | i18n }} + {{ "zipPostalCodeLabel" | i18n }} From 9d849d22341a61777dea6b417fbac9bfe077c33c Mon Sep 17 00:00:00 2001 From: neuronull <9162534+neuronull@users.noreply.github.com> Date: Mon, 27 Oct 2025 06:39:40 -0700 Subject: [PATCH 41/73] Convert `log` crate Records to `tracing` Events for desktop native. (#16827) * Convert `log` crate Records to `tracing` Events for desktop native. * sort deps * use the feature on tracing_subscriber --- apps/desktop/desktop_native/Cargo.toml | 2 +- apps/desktop/desktop_native/napi/src/lib.rs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index c0fe0b46f58..2168eaa0068 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -68,7 +68,7 @@ tokio = "=1.45.0" tokio-stream = "=0.1.15" tokio-util = "=0.7.13" tracing = "=0.1.41" -tracing-subscriber = { version = "=0.3.20", features = ["fmt", "env-filter"] } +tracing-subscriber = { version = "=0.3.20", features = ["fmt", "env-filter", "tracing-log"] } typenum = "=1.18.0" uniffi = "=0.28.3" widestring = "=1.2.0" diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index a193e44d6df..09f63f7854b 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -1051,6 +1051,10 @@ pub mod logging { // overriding the default directive for matching targets. .from_env_lossy(); + // With the `tracing-log` feature enabled for the `tracing_subscriber`, + // the registry below will initialize a log compatibility layer, which allows + // the subscriber to consume log::Records as though they were tracing Events. + // https://docs.rs/tracing-subscriber/latest/tracing_subscriber/util/trait.SubscriberInitExt.html#method.init tracing_subscriber::registry() .with(filter) .with(JsLayer) From 942f403ed0726f5e035317e3459bd6771b4e8334 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Mon, 27 Oct 2025 08:41:22 -0500 Subject: [PATCH 42/73] Fix restart subscription modal showing twice from switcher (#16973) --- .../app/layouts/org-switcher/org-switcher.component.html | 1 - .../app/layouts/org-switcher/org-switcher.component.ts | 9 +-------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/apps/web/src/app/layouts/org-switcher/org-switcher.component.html b/apps/web/src/app/layouts/org-switcher/org-switcher.component.html index 96d17e7ada4..a9acddeb0b8 100644 --- a/apps/web/src/app/layouts/org-switcher/org-switcher.component.html +++ b/apps/web/src/app/layouts/org-switcher/org-switcher.component.html @@ -22,7 +22,6 @@ [route]="['../', org.id]" (mainContentClicked)="toggle()" [routerLinkActiveOptions]="{ exact: true }" - (click)="showInactiveSubscriptionDialog(org)" > - await firstValueFrom( - this.organizationWarningsService.showInactiveSubscriptionDialog$(organization), - ); } From abc6e54bb9d6c1e5d0e4f7e87cbc5aaef9da689e Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Mon, 27 Oct 2025 15:13:17 +0100 Subject: [PATCH 43/73] Platform - Prefer signal & change detection (#16946) --- .../platform/popup/components/pop-out.component.ts | 4 ++++ .../popup/view-cache/popup-router-cache.spec.ts | 2 ++ .../popup/view-cache/popup-view-cache.spec.ts | 4 ++++ .../src/platform/components/approve-ssh-request.ts | 2 ++ .../account-fingerprint.component.ts | 8 ++++++++ .../onboarding/onboarding-task.component.ts | 12 ++++++++++++ .../components/onboarding/onboarding.component.ts | 8 ++++++++ .../src/platform/guard/feature-flag.guard.spec.ts | 2 ++ 8 files changed, 42 insertions(+) diff --git a/apps/browser/src/platform/popup/components/pop-out.component.ts b/apps/browser/src/platform/popup/components/pop-out.component.ts index 320fa6f05ab..fd2acbd8aa7 100644 --- a/apps/browser/src/platform/popup/components/pop-out.component.ts +++ b/apps/browser/src/platform/popup/components/pop-out.component.ts @@ -7,12 +7,16 @@ import { IconButtonModule } from "@bitwarden/components"; import BrowserPopupUtils from "../../browser/browser-popup-utils"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-pop-out", templateUrl: "pop-out.component.html", imports: [CommonModule, JslibModule, IconButtonModule], }) export class PopOutComponent implements OnInit { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() show = true; constructor(private platformUtilsService: PlatformUtilsService) {} diff --git a/apps/browser/src/platform/popup/view-cache/popup-router-cache.spec.ts b/apps/browser/src/platform/popup/view-cache/popup-router-cache.spec.ts index 3304a99023e..835a8eebd2c 100644 --- a/apps/browser/src/platform/popup/view-cache/popup-router-cache.spec.ts +++ b/apps/browser/src/platform/popup/view-cache/popup-router-cache.spec.ts @@ -13,6 +13,8 @@ import { PopupRouterCacheService, popupRouterCacheGuard } from "./popup-router-c const flushPromises = async () => await new Promise(process.nextTick); +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: "", standalone: false, diff --git a/apps/browser/src/platform/popup/view-cache/popup-view-cache.spec.ts b/apps/browser/src/platform/popup/view-cache/popup-view-cache.spec.ts index 60baf94eeae..a18d51878ee 100644 --- a/apps/browser/src/platform/popup/view-cache/popup-view-cache.spec.ts +++ b/apps/browser/src/platform/popup/view-cache/popup-view-cache.spec.ts @@ -19,12 +19,16 @@ import { import { PopupViewCacheService } from "./popup-view-cache.service"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: "", standalone: false, }) export class EmptyComponent {} +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: "", standalone: false, diff --git a/apps/desktop/src/platform/components/approve-ssh-request.ts b/apps/desktop/src/platform/components/approve-ssh-request.ts index 8cd63e0b1ac..1741124774d 100644 --- a/apps/desktop/src/platform/components/approve-ssh-request.ts +++ b/apps/desktop/src/platform/components/approve-ssh-request.ts @@ -21,6 +21,8 @@ export interface ApproveSshRequestParams { action: string; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-approve-ssh-request", templateUrl: "approve-ssh-request.html", diff --git a/apps/web/src/app/shared/components/account-fingerprint/account-fingerprint.component.ts b/apps/web/src/app/shared/components/account-fingerprint/account-fingerprint.component.ts index 256c8d6af34..eb84868dca1 100644 --- a/apps/web/src/app/shared/components/account-fingerprint/account-fingerprint.component.ts +++ b/apps/web/src/app/shared/components/account-fingerprint/account-fingerprint.component.ts @@ -6,14 +6,22 @@ import { KeyService } from "@bitwarden/key-management"; import { SharedModule } from "../../shared.module"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-account-fingerprint", templateUrl: "account-fingerprint.component.html", imports: [SharedModule], }) export class AccountFingerprintComponent implements OnInit { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() fingerprintMaterial: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() publicKeyBuffer: Uint8Array; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() fingerprintLabel: string; protected fingerprint: string; diff --git a/apps/web/src/app/shared/components/onboarding/onboarding-task.component.ts b/apps/web/src/app/shared/components/onboarding/onboarding-task.component.ts index f9798ec7f0f..277a4d2d26e 100644 --- a/apps/web/src/app/shared/components/onboarding/onboarding-task.component.ts +++ b/apps/web/src/app/shared/components/onboarding/onboarding-task.component.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { Component, Input } from "@angular/core"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-onboarding-task", templateUrl: "./onboarding-task.component.html", @@ -11,18 +13,28 @@ import { Component, Input } from "@angular/core"; standalone: false, }) export class OnboardingTaskComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() completed = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() icon = "bwi-info-circle"; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() title: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() route: string | any[]; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() isDisabled: boolean = false; diff --git a/apps/web/src/app/shared/components/onboarding/onboarding.component.ts b/apps/web/src/app/shared/components/onboarding/onboarding.component.ts index 5ead9fcc10b..832e7964cce 100644 --- a/apps/web/src/app/shared/components/onboarding/onboarding.component.ts +++ b/apps/web/src/app/shared/components/onboarding/onboarding.component.ts @@ -4,15 +4,23 @@ import { Component, ContentChildren, EventEmitter, Input, Output, QueryList } fr import { OnboardingTaskComponent } from "./onboarding-task.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-onboarding", templateUrl: "./onboarding.component.html", standalone: false, }) export class OnboardingComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ContentChildren(OnboardingTaskComponent) tasks: QueryList; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() title: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() dismiss = new EventEmitter(); protected open = true; diff --git a/libs/angular/src/platform/guard/feature-flag.guard.spec.ts b/libs/angular/src/platform/guard/feature-flag.guard.spec.ts index 3bc8b085a7d..fa6d82b49e0 100644 --- a/libs/angular/src/platform/guard/feature-flag.guard.spec.ts +++ b/libs/angular/src/platform/guard/feature-flag.guard.spec.ts @@ -12,6 +12,8 @@ import { I18nMockService, ToastService } from "@bitwarden/components/src"; import { canAccessFeature } from "./feature-flag.guard"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: "", standalone: false }) export class EmptyComponent {} From 64590cb3c82d9c8bc4173c5d1ec82ed07f1fd953 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Mon, 27 Oct 2025 15:17:20 +0100 Subject: [PATCH 44/73] [PM-25911] Add commercial sdk internal as dependency (#16883) * feat: add commercial sdk as optional dependency * feat: add alias to CLI * feat: add alias to browser * feat: add alias to web * fix: revert optional - we cant omit optional dependencies or the builds break * feat: remove commercial package from browser build * feat: remove commercial package from cli build * feat: remove commercial package from web build * chore: add commercial sdk to renovate * fix: windows cli workflow * fix: accidental change * feat: add lint for version string * undo weird merge changes --- .github/renovate.json5 | 1 + .github/workflows/build-browser.yml | 14 ++++++++++++ .github/workflows/build-cli.yml | 18 +++++++++++---- .github/workflows/build-web.yml | 10 +++++++++ .github/workflows/lint.yml | 3 +++ .npmrc | 2 +- apps/browser/webpack.base.js | 5 ++++- apps/cli/webpack.base.js | 2 ++ apps/web/Dockerfile | 6 +++++ apps/web/webpack.base.js | 2 ++ .../bit-browser/webpack.config.js | 12 ++++++++++ bitwarden_license/bit-cli/webpack.config.js | 12 ++++++++++ bitwarden_license/bit-web/webpack.config.js | 12 ++++++++++ package-lock.json | 22 +++++++++++++++++++ package.json | 2 ++ scripts/sdk-internal-versions.ts | 22 +++++++++++++++++++ 16 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 scripts/sdk-internal-versions.ts diff --git a/.github/renovate.json5 b/.github/renovate.json5 index f898df460c9..ae7c2b023cb 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -139,6 +139,7 @@ "@babel/core", "@babel/preset-env", "@bitwarden/sdk-internal", + "@bitwarden/commercial-sdk-internal", "@electron/fuses", "@electron/notarize", "@electron/rebuild", diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index 5980ef507cc..1c805e8efbe 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -219,12 +219,14 @@ jobs: archive_name_prefix: "" npm_command_prefix: "dist:" readable: "open source license" + type: "oss" - build_prefix: "bit-" artifact_prefix: "bit-" source_archive_name_prefix: "bit-" archive_name_prefix: "bit-" npm_command_prefix: "dist:bit:" readable: "commercial license" + type: "commercial" browser: - name: "chrome" npm_command_suffix: "chrome" @@ -279,6 +281,11 @@ jobs: run: npm ci working-directory: browser-source/ + - name: Remove commercial packages + if: ${{ matrix.license_type.type == 'oss' }} + run: rm -rf node_modules/@bitwarden/commercial-sdk-internal + working-directory: browser-source/ + - name: Download SDK artifacts if: ${{ inputs.sdk_branch != '' }} uses: bitwarden/gh-actions/download-artifacts@main @@ -350,11 +357,13 @@ jobs: archive_name_prefix: "" npm_command_prefix: "dist:" readable: "open source license" + type: "oss" - build_prefix: "bit-" artifact_prefix: "bit-" archive_name_prefix: "bit-" npm_command_prefix: "dist:bit:" readable: "commercial license" + type: "commercial" env: _BUILD_NUMBER: ${{ needs.setup.outputs.adj_build_number }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} @@ -461,6 +470,11 @@ jobs: run: npm ci working-directory: ./ + - name: Remove commercial packages + if: ${{ matrix.license_type.type == 'oss' }} + run: rm -rf node_modules/@bitwarden/commercial-sdk-internal + working-directory: ./ + - name: Download SDK Artifacts if: ${{ inputs.sdk_branch != '' }} uses: bitwarden/gh-actions/download-artifacts@main diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 1f7b35f3307..c2abbdf5e5c 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -98,8 +98,8 @@ jobs: ] license_type: [ - { build_prefix: "oss", artifact_prefix: "-oss", readable: "open source license" }, - { build_prefix: "bit", artifact_prefix: "", readable: "commercial license" } + { type: "oss", build_prefix: "oss", artifact_prefix: "-oss", readable: "open source license" }, + { type: "commercial", build_prefix: "bit", artifact_prefix: "", readable: "commercial license" } ] runs-on: ${{ matrix.os.distro }} needs: setup @@ -140,6 +140,11 @@ jobs: run: npm ci working-directory: ./ + - name: Remove commercial packages + if: ${{ matrix.license_type.type == 'oss' }} + run: rm -rf node_modules/@bitwarden/commercial-sdk-internal + working-directory: ./ + - name: Download SDK Artifacts if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} uses: bitwarden/gh-actions/download-artifacts@main @@ -291,8 +296,8 @@ jobs: matrix: license_type: [ - { build_prefix: "oss", artifact_prefix: "-oss", readable: "open source license" }, - { build_prefix: "bit", artifact_prefix: "", readable: "commercial license" } + { type: "oss", build_prefix: "oss", artifact_prefix: "-oss", readable: "open source license" }, + { type: "commercial", build_prefix: "bit", artifact_prefix: "", readable: "commercial license" } ] runs-on: windows-2022 permissions: @@ -410,6 +415,11 @@ jobs: run: npm ci working-directory: ./ + - name: Remove commercial packages + if: ${{ matrix.license_type.type == 'oss' }} + run: Remove-Item -Recurse -Force -ErrorAction SilentlyContinue "node_modules/@bitwarden/commercial-sdk-internal" + working-directory: ./ + - name: Download SDK Artifacts if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} uses: bitwarden/gh-actions/download-artifacts@main diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index ee7444f13a9..0ea3ad7af78 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -99,34 +99,43 @@ jobs: matrix: include: - artifact_name: selfhosted-open-source + license_type: "oss" image_name: web-oss npm_command: dist:oss:selfhost - artifact_name: cloud-COMMERCIAL + license_type: "commercial" image_name: web-cloud npm_command: dist:bit:cloud - artifact_name: selfhosted-COMMERCIAL + license_type: "commercial" image_name: web npm_command: dist:bit:selfhost - artifact_name: selfhosted-DEV + license_type: "commercial" image_name: web npm_command: build:bit:selfhost:dev git_metadata: true - artifact_name: cloud-QA + license_type: "commercial" image_name: web-qa-cloud npm_command: build:bit:qa git_metadata: true - artifact_name: ee + license_type: "commercial" image_name: web-ee npm_command: build:bit:ee git_metadata: true - artifact_name: cloud-euprd + license_type: "commercial" image_name: web-euprd npm_command: build:bit:euprd - artifact_name: cloud-euqa + license_type: "commercial" image_name: web-euqa npm_command: build:bit:euqa git_metadata: true - artifact_name: cloud-usdev + license_type: "commercial" image_name: web-usdev npm_command: build:bit:usdev git_metadata: true @@ -269,6 +278,7 @@ jobs: build-args: | NODE_VERSION=${{ env._NODE_VERSION }} NPM_COMMAND=${{ matrix.npm_command }} + LICENSE_TYPE=${{ matrix.license_type }} context: . file: apps/web/Dockerfile load: true diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index bc78462fdb5..21786339299 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -75,6 +75,9 @@ jobs: - name: Lint unowned dependencies run: npm run lint:dep-ownership + - name: Lint sdk-internal versions + run: npm run lint:sdk-internal-versions + - name: Run linter run: npm run lint diff --git a/.npmrc b/.npmrc index 421cf18217d..38a7eb153c0 100644 --- a/.npmrc +++ b/.npmrc @@ -1,4 +1,4 @@ save-exact=true # Increase available heap size to avoid running out of memory when compiling. # This applies to all npm scripts in this repository. -node-options=--max-old-space-size=8192 \ No newline at end of file +node-options=--max-old-space-size=8192 diff --git a/apps/browser/webpack.base.js b/apps/browser/webpack.base.js index 734a46ac187..4bc2a90c4ff 100644 --- a/apps/browser/webpack.base.js +++ b/apps/browser/webpack.base.js @@ -36,7 +36,8 @@ const DEFAULT_PARAMS = { * outputPath?: string; * mode?: string; * env?: string; - * additionalEntries?: { [outputPath: string]: string } + * additionalEntries?: { [outputPath: string]: string }; + * importAliases?: import("webpack").ResolveOptions["alias"]; * }} params - The input parameters for building the config. */ module.exports.buildConfig = function buildConfig(params) { @@ -362,6 +363,7 @@ module.exports.buildConfig = function buildConfig(params) { path: require.resolve("path-browserify"), }, cache: true, + alias: params.importAliases, }, output: { filename: "[name].js", @@ -482,6 +484,7 @@ module.exports.buildConfig = function buildConfig(params) { path: require.resolve("path-browserify"), }, cache: true, + alias: params.importAliases, }, dependencies: ["main"], plugins: [...requiredPlugins, new AngularCheckPlugin()], diff --git a/apps/cli/webpack.base.js b/apps/cli/webpack.base.js index 01d5fc5b175..532b0a747a0 100644 --- a/apps/cli/webpack.base.js +++ b/apps/cli/webpack.base.js @@ -31,6 +31,7 @@ const DEFAULT_PARAMS = { * localesPath?: string; * externalsModulesDir?: string; * watch?: boolean; + * importAliases?: import("webpack").ResolveOptions["alias"]; * }} params */ module.exports.buildConfig = function buildConfig(params) { @@ -95,6 +96,7 @@ module.exports.buildConfig = function buildConfig(params) { symlinks: false, modules: params.modulesPath, plugins: [new TsconfigPathsPlugin({ configFile: params.tsConfig })], + alias: params.importAliases, }, output: { filename: "[name].js", diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile index 6017d60df5f..6d27e12537a 100644 --- a/apps/web/Dockerfile +++ b/apps/web/Dockerfile @@ -9,6 +9,12 @@ COPY package*.json ./ COPY . . RUN npm ci +# Remove commercial packages if LICENSE_TYPE is not 'commercial' +ARG LICENSE_TYPE=oss +RUN if [ "${LICENSE_TYPE}" != "commercial" ] ; then \ + rm -rf node_modules/@bitwarden/commercial-sdk-internal ; \ + fi + WORKDIR /source/apps/web ARG NPM_COMMAND=dist:bit:selfhost RUN npm run ${NPM_COMMAND} diff --git a/apps/web/webpack.base.js b/apps/web/webpack.base.js index 56fd6c7faf5..f1e627a58a8 100644 --- a/apps/web/webpack.base.js +++ b/apps/web/webpack.base.js @@ -36,6 +36,7 @@ const DEFAULT_PARAMS = { * outputPath?: string; * mode?: string; * env?: string; + * importAliases?: import("webpack").ResolveOptions["alias"]; * }} params */ module.exports.buildConfig = function buildConfig(params) { @@ -460,6 +461,7 @@ module.exports.buildConfig = function buildConfig(params) { process: false, path: require.resolve("path-browserify"), }, + alias: params.importAliases, }, output: { filename: "[name].[contenthash].js", diff --git a/bitwarden_license/bit-browser/webpack.config.js b/bitwarden_license/bit-browser/webpack.config.js index 1c6ab51549f..a0b1870721b 100644 --- a/bitwarden_license/bit-browser/webpack.config.js +++ b/bitwarden_license/bit-browser/webpack.config.js @@ -36,6 +36,12 @@ module.exports = (webpackConfig, context) => { : context.options.outputPath, mode: mode, env: ENV, + importAliases: [ + { + name: "@bitwarden/sdk-internal", + alias: "@bitwarden/commercial-sdk-internal", + }, + ], }); } else { // npm build configuration @@ -49,6 +55,12 @@ module.exports = (webpackConfig, context) => { entry: path.resolve(__dirname, "src/platform/background.ts"), }, tsConfig: path.resolve(__dirname, "tsconfig.json"), + importAliases: [ + { + name: "@bitwarden/sdk-internal", + alias: "@bitwarden/commercial-sdk-internal", + }, + ], }); } }; diff --git a/bitwarden_license/bit-cli/webpack.config.js b/bitwarden_license/bit-cli/webpack.config.js index f746da40761..6d31d0b5e96 100644 --- a/bitwarden_license/bit-cli/webpack.config.js +++ b/bitwarden_license/bit-cli/webpack.config.js @@ -24,6 +24,12 @@ module.exports = (webpackConfig, context) => { localesPath: "apps/cli/src/locales", externalsModulesDir: "node_modules", watch: context.options.watch || false, + importAliases: [ + { + name: "@bitwarden/sdk-internal", + alias: "@bitwarden/commercial-sdk-internal", + }, + ], }); } else { // npm build configuration @@ -43,6 +49,12 @@ module.exports = (webpackConfig, context) => { modulesPath: [path.resolve("../../node_modules")], localesPath: "../../apps/cli/src/locales", externalsModulesDir: "../../node_modules", + importAliases: [ + { + name: "@bitwarden/sdk-internal", + alias: "@bitwarden/commercial-sdk-internal", + }, + ], }); } }; diff --git a/bitwarden_license/bit-web/webpack.config.js b/bitwarden_license/bit-web/webpack.config.js index 6ac1efdc192..6433eee59f6 100644 --- a/bitwarden_license/bit-web/webpack.config.js +++ b/bitwarden_license/bit-web/webpack.config.js @@ -17,6 +17,12 @@ module.exports = (webpackConfig, context) => { context.context && context.context.root ? path.resolve(context.context.root, context.options.outputPath) : context.options.outputPath, + importAliases: [ + { + name: "@bitwarden/sdk-internal", + alias: "@bitwarden/commercial-sdk-internal", + }, + ], }); } else { return buildConfig({ @@ -26,6 +32,12 @@ module.exports = (webpackConfig, context) => { entryModule: "bitwarden_license/bit-web/src/app/app.module#AppModule", }, tsConfig: path.resolve(__dirname, "tsconfig.build.json"), + importAliases: [ + { + name: "@bitwarden/sdk-internal", + alias: "@bitwarden/commercial-sdk-internal", + }, + ], }); } }; diff --git a/package-lock.json b/package-lock.json index 8ce60e0826f..747576d4ca2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@angular/platform-browser": "19.2.14", "@angular/platform-browser-dynamic": "19.2.14", "@angular/router": "19.2.14", + "@bitwarden/commercial-sdk-internal": "0.2.0-main.357", "@bitwarden/sdk-internal": "0.2.0-main.357", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", @@ -4605,6 +4606,27 @@ "resolved": "libs/client-type", "link": true }, + "node_modules/@bitwarden/commercial-sdk-internal": { + "version": "0.2.0-main.357", + "resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.357.tgz", + "integrity": "sha512-eIArJelJKwG+aEGbtdhc5dKRBFopmyGJl+ClUQGJUFHzfrPGDcaSI04a/sSUK0NtbaxQOsf8qSvk+iKvISkKmw==", + "license": "BITWARDEN SOFTWARE DEVELOPMENT KIT LICENSE AGREEMENT", + "dependencies": { + "type-fest": "^4.41.0" + } + }, + "node_modules/@bitwarden/commercial-sdk-internal/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@bitwarden/common": { "resolved": "libs/common", "link": true diff --git a/package.json b/package.json index 89e127488b2..c241e07e2e1 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "test:types": "node ./scripts/test-types.js", "test:locales": "tsc --project ./scripts/tsconfig.json && node ./scripts/dist/test-locales.js", "lint:dep-ownership": "tsc --project ./scripts/tsconfig.json && node ./scripts/dist/dep-ownership.js", + "lint:sdk-internal-versions": "tsc --project ./scripts/tsconfig.json && node ./scripts/dist/sdk-internal-versions.js", "docs:json": "compodoc -p ./tsconfig.json -e json -d . --disableRoutesGraph", "storybook": "ng run components:storybook", "build-storybook": "ng run components:build-storybook", @@ -160,6 +161,7 @@ "@angular/platform-browser-dynamic": "19.2.14", "@angular/router": "19.2.14", "@bitwarden/sdk-internal": "0.2.0-main.357", + "@bitwarden/commercial-sdk-internal": "0.2.0-main.357", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", diff --git a/scripts/sdk-internal-versions.ts b/scripts/sdk-internal-versions.ts new file mode 100644 index 00000000000..c442772e553 --- /dev/null +++ b/scripts/sdk-internal-versions.ts @@ -0,0 +1,22 @@ +/* eslint-disable no-console */ + +/// Ensure that `sdk-internal` and `commercial-sdk-internal` dependencies have matching versions. + +import fs from "fs"; +import path from "path"; + +const packageJson = JSON.parse( + fs.readFileSync(path.join(__dirname, "..", "..", "package.json"), "utf8"), +); + +const sdkInternal = packageJson.dependencies["@bitwarden/sdk-internal"]; +const commercialSdkInternal = packageJson.dependencies["@bitwarden/commercial-sdk-internal"]; + +if (sdkInternal !== commercialSdkInternal) { + console.error( + `Version mismatch between @bitwarden/sdk-internal (${sdkInternal}) and @bitwarden/commercial-sdk-internal (${commercialSdkInternal}), must be an exact match.`, + ); + process.exit(1); +} + +console.log(`All dependencies have matching versions: ${sdkInternal}`); From ea4b6779a57f810357b23fb62de70cd428564acb Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Mon, 27 Oct 2025 10:35:18 -0400 Subject: [PATCH 45/73] [PM-26373] Update invitation accepted toast copy (#17021) * update copy * update copy * update i18n.t * use toast service, remove toast title * fix spelling --- .../accept-organization.component.ts | 15 ++++++++------- apps/web/src/locales/en/messages.json | 4 ++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.component.ts b/apps/web/src/app/auth/organization-invite/accept-organization.component.ts index f98a62f91ea..cb1175a7002 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.component.ts +++ b/apps/web/src/app/auth/organization-invite/accept-organization.component.ts @@ -11,6 +11,7 @@ import { OrganizationInvite } from "@bitwarden/common/auth/services/organization import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; import { BaseAcceptComponent } from "../../common/base.accept.component"; @@ -35,6 +36,7 @@ export class AcceptOrganizationComponent extends BaseAcceptComponent { private acceptOrganizationInviteService: AcceptOrganizationInviteService, private organizationInviteService: OrganizationInviteService, private accountService: AccountService, + private toastService: ToastService, ) { super(router, platformUtilsService, i18nService, route, authService); } @@ -51,14 +53,13 @@ export class AcceptOrganizationComponent extends BaseAcceptComponent { return; } - this.platformUtilService.showToast( - "success", - this.i18nService.t("inviteAccepted"), - invite.initOrganization + this.toastService.showToast({ + message: invite.initOrganization ? this.i18nService.t("inviteInitAcceptedDesc") - : this.i18nService.t("inviteAcceptedDesc"), - { timeout: 10000 }, - ); + : this.i18nService.t("invitationAcceptedDesc"), + variant: "success", + timeout: 10000, + }); await this.router.navigate(["/vault"]); } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index f88af8aa1a7..72ca4d73976 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -4452,8 +4452,8 @@ "inviteAccepted": { "message": "Invitation accepted" }, - "inviteAcceptedDesc": { - "message": "You can access this organization once an administrator confirms your membership. We'll send you an email when that happens." + "invitationAcceptedDesc": { + "message": "Successfully accepted your invitation." }, "inviteInitAcceptedDesc": { "message": "You can now access this organization." From fd4568974520b75b67705380c0f6adc32acb4d0a Mon Sep 17 00:00:00 2001 From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Date: Mon, 27 Oct 2025 09:55:31 -0500 Subject: [PATCH 46/73] [PM-27342] Fix state migration (#17018) * Fix migration * Update test --- .../73-add-master-password-unlock-data.spec.ts | 12 ++++++++++++ .../migrations/73-add-master-password-unlock-data.ts | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/libs/state/src/state-migrations/migrations/73-add-master-password-unlock-data.spec.ts b/libs/state/src/state-migrations/migrations/73-add-master-password-unlock-data.spec.ts index 28e65216653..2956b1cbcd2 100644 --- a/libs/state/src/state-migrations/migrations/73-add-master-password-unlock-data.spec.ts +++ b/libs/state/src/state-migrations/migrations/73-add-master-password-unlock-data.spec.ts @@ -97,6 +97,18 @@ describe("AddMasterPasswordUnlockData", () => { user_user1_kdfConfig_kdfConfig: { kdfType: 0, iterations: 600000 }, }); }); + + it("handles users with missing global accounts", async () => { + const output = await runMigrator(sut, { + global_account_accounts: { user_user1: null }, + user_user1_kdfConfig_kdfConfig: { kdfType: 0, iterations: 600000 }, + }); + + expect(output).toEqual({ + global_account_accounts: { user_user1: null }, + user_user1_kdfConfig_kdfConfig: { kdfType: 0, iterations: 600000 }, + }); + }); }); describe("rollback", () => { diff --git a/libs/state/src/state-migrations/migrations/73-add-master-password-unlock-data.ts b/libs/state/src/state-migrations/migrations/73-add-master-password-unlock-data.ts index b9833f439a6..321df7d5cfc 100644 --- a/libs/state/src/state-migrations/migrations/73-add-master-password-unlock-data.ts +++ b/libs/state/src/state-migrations/migrations/73-add-master-password-unlock-data.ts @@ -32,7 +32,7 @@ type Account = { export class AddMasterPasswordUnlockData extends Migrator<72, 73> { async migrate(helper: MigrationHelper): Promise { async function migrateAccount(userId: string, account: Account) { - const email = account.email; + const email = account?.email; const kdfConfig = await helper.getFromUser(userId, KDF_CONFIG_DISK); const masterKeyEncryptedUserKey = await helper.getFromUser( userId, From af6e19335d751d4eebe6cf68ab7e44b9c7bea9ed Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Mon, 27 Oct 2025 16:13:11 +0100 Subject: [PATCH 47/73] Vault - Prefer signal & change detection (#16947) --- .../at-risk-password-callout.component.ts | 2 + .../at-risk-carousel-dialog.component.ts | 4 ++ .../at-risk-passwords.component.spec.ts | 14 ++++++ .../at-risk-passwords.component.ts | 2 + .../add-edit/add-edit-v2.component.ts | 2 + .../assign-collections.component.ts | 2 + .../attachments-v2.component.spec.ts | 10 ++++ .../attachments/attachments-v2.component.ts | 2 + .../open-attachments.component.ts | 4 ++ .../autofill-vault-list-items.component.ts | 4 +- .../blocked-injection-banner.component.ts | 2 + .../intro-carousel.component.ts | 2 + .../item-copy-actions.component.ts | 4 ++ .../item-more-options.component.ts | 8 +++ .../new-item-dropdown-v2.component.ts | 4 ++ .../vault-generator-dialog.component.spec.ts | 10 ++++ .../vault-generator-dialog.component.ts | 2 + .../vault-header/vault-header-v2.component.ts | 4 ++ .../vault-list-filters.component.ts | 2 + .../vault-list-items-container.component.ts | 40 +++++++++++++++ .../vault-password-history-v2.component.ts | 2 + .../vault-search/vault-v2-search.component.ts | 2 + .../components/vault-v2/vault-v2.component.ts | 4 ++ .../vault-v2/view-v2/view-v2.component.ts | 2 + .../services/vault-popup-section.service.ts | 8 +-- .../settings/appearance-v2.component.spec.ts | 10 ++++ .../popup/settings/appearance-v2.component.ts | 2 + .../vault/popup/settings/archive.component.ts | 2 + .../settings/download-bitwarden.component.ts | 2 + .../settings/folders-v2.component.spec.ts | 10 ++++ .../popup/settings/folders-v2.component.ts | 2 + .../more-from-bitwarden-page-v2.component.ts | 2 + .../trash-list-items-container.component.ts | 4 ++ .../settings/vault-settings-v2.component.ts | 6 ++- .../assign-collections-desktop.component.ts | 2 + .../credential-generator-dialog.component.ts | 2 + .../vault/app/vault/item-footer.component.ts | 24 +++++++++ .../filters/collection-filter.component.ts | 2 + .../filters/folder-filter.component.ts | 2 + .../filters/organization-filter.component.ts | 2 + .../filters/status-filter.component.ts | 2 + .../filters/type-filter.component.ts | 2 + .../vault-filter/vault-filter.component.ts | 2 + .../app/vault/vault-items-v2.component.ts | 2 + .../src/vault/app/vault/vault-v2.component.ts | 10 ++++ .../assign-collections-web.component.ts | 2 + ...wser-extension-prompt-install.component.ts | 2 + .../browser-extension-prompt.component.ts | 2 + .../manually-open-extension.component.ts | 2 + .../add-extension-later-dialog.component.ts | 2 + .../add-extension-videos.component.ts | 4 ++ .../setup-extension.component.ts | 2 + .../vault-item-dialog.component.ts | 6 +++ .../vault-items/vault-cipher-row.component.ts | 46 +++++++++++++++++ .../vault-collection-row.component.ts | 32 ++++++++++++ .../vault-items/vault-items.component.ts | 50 +++++++++++++++++++ .../web-generator-dialog.component.spec.ts | 10 ++++ .../web-generator-dialog.component.ts | 2 + .../individual-vault/add-edit-v2.component.ts | 2 + .../bulk-delete-dialog.component.ts | 2 + .../bulk-move-dialog.component.ts | 2 + .../organization-name-badge.component.ts | 8 +++ .../vault-banners/vault-banners.component.ts | 4 ++ .../organization-options.component.ts | 2 + .../components/vault-filter.component.ts | 10 ++++ .../vault-filter-section.component.ts | 6 +++ .../vault-header/vault-header.component.ts | 20 ++++++++ .../vault-onboarding.component.ts | 8 +++ .../vault/individual-vault/vault.component.ts | 6 +++ .../vault/settings/purge-vault.component.ts | 2 + .../components/folder-add-edit.component.ts | 6 +++ .../src/vault/components/icon.component.ts | 6 +-- .../spotlight/spotlight.component.ts | 16 ++++++ .../vault/components/vault-items.component.ts | 10 ++++ .../components/collection-filter.component.ts | 12 +++++ .../components/folder-filter.component.ts | 16 ++++++ .../organization-filter.component.ts | 16 ++++++ .../components/status-filter.component.ts | 10 ++++ .../components/type-filter.component.ts | 12 +++++ .../components/vault-filter.component.ts | 18 +++++++ ...ditional-options-section.component.spec.ts | 4 ++ .../additional-options-section.component.ts | 6 +++ .../cipher-attachments.component.spec.ts | 8 +++ .../cipher-attachments.component.ts | 18 +++++++ .../delete-attachment.component.ts | 10 ++++ .../advanced-uri-option-dialog.component.ts | 2 + .../autofill-options.component.ts | 4 ++ .../autofill-options/uri-option.component.ts | 18 +++++++ .../card-details-section.component.ts | 6 +++ .../components/cipher-form.component.ts | 18 +++++++ .../cipher-form-generator.component.spec.ts | 8 +++ .../cipher-form-generator.component.ts | 12 +++++ .../add-edit-custom-field-dialog.component.ts | 2 + .../custom-fields/custom-fields.component.ts | 8 +++ .../components/identity/identity.component.ts | 6 +++ .../item-details-section.component.ts | 6 +++ .../login-details-section.component.spec.ts | 2 + .../login-details-section.component.ts | 2 + .../new-item-nudge.component.ts | 4 +- .../sshkey-section.component.ts | 6 +++ .../additional-options.component.ts | 4 ++ .../attachments-v2-view.component.ts | 8 +++ .../attachments/attachments-v2.component.ts | 2 + .../autofill-options-view.component.ts | 6 +++ .../card-details-view.component.ts | 4 ++ .../src/cipher-view/cipher-view.component.ts | 10 ++++ .../custom-fields-v2.component.ts | 4 ++ .../item-details/item-details-v2.component.ts | 2 + .../item-history/item-history-v2.component.ts | 4 ++ .../login-credentials-view.component.ts | 12 +++++ .../read-only-cipher-card.component.ts | 4 ++ .../sshkey-sections/sshkey-view.component.ts | 4 ++ .../view-identity-sections.component.ts | 4 ++ .../add-edit-folder-dialog.component.ts | 6 +++ .../assign-collections.component.ts | 12 +++++ .../components/can-delete-cipher.directive.ts | 2 + .../carousel-button.component.ts | 10 ++++ .../carousel-content.component.spec.ts | 4 ++ .../carousel-content.component.ts | 4 ++ .../carousel-slide.component.spec.ts | 2 + .../carousel-slide.component.ts | 10 ++++ .../carousel/carousel.component.spec.ts | 2 + .../components/carousel/carousel.component.ts | 18 +++++++ .../components/copy-cipher-field.directive.ts | 7 ++- .../components/dark-image-source.directive.ts | 2 +- .../decryption-failure-dialog.component.ts | 2 + .../download-attachment.component.ts | 12 +++++ .../new-cipher-menu.component.ts | 10 ++++ .../src/components/org-icon.directive.ts | 4 ++ .../password-history-view.component.ts | 4 ++ .../password-history.component.ts | 2 + .../components/password-reprompt.component.ts | 2 + ...permit-cipher-details-popover.component.ts | 2 + .../totp-countdown.component.ts | 6 +++ 134 files changed, 918 insertions(+), 13 deletions(-) diff --git a/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.ts b/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.ts index c3d4f461d70..c37131b3ff1 100644 --- a/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.ts +++ b/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.ts @@ -10,6 +10,8 @@ import { AnchorLinkDirective, CalloutModule, BannerModule } from "@bitwarden/com import { I18nPipe } from "@bitwarden/ui-common"; import { AtRiskPasswordCalloutData, AtRiskPasswordCalloutService } from "@bitwarden/vault"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-at-risk-password-callout", imports: [ diff --git a/apps/browser/src/vault/popup/components/at-risk-carousel-dialog/at-risk-carousel-dialog.component.ts b/apps/browser/src/vault/popup/components/at-risk-carousel-dialog/at-risk-carousel-dialog.component.ts index 08c466d21a9..f81bccc760c 100644 --- a/apps/browser/src/vault/popup/components/at-risk-carousel-dialog/at-risk-carousel-dialog.component.ts +++ b/apps/browser/src/vault/popup/components/at-risk-carousel-dialog/at-risk-carousel-dialog.component.ts @@ -17,6 +17,8 @@ export const AtRiskCarouselDialogResult = { type AtRiskCarouselDialogResult = UnionOfValues; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-at-risk-carousel-dialog", templateUrl: "./at-risk-carousel-dialog.component.html", @@ -32,6 +34,8 @@ type AtRiskCarouselDialogResult = UnionOfValues`, }) class MockPopupHeaderComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() pageTitle: string | undefined; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() backAction: (() => void) | undefined; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "popup-page", template: ``, }) class MockPopupPageComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() loading: boolean | undefined; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-vault-icon", template: ``, }) class MockAppIcon { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() cipher: CipherView | undefined; } diff --git a/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.ts b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.ts index 6918bedb9bf..3eeb2d1917b 100644 --- a/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.ts +++ b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.ts @@ -58,6 +58,8 @@ import { import { AtRiskPasswordPageService } from "./at-risk-password-page.service"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ imports: [ PopupPageComponent, diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts index 463819b96e4..60e44cefbdf 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts @@ -131,6 +131,8 @@ class QueryParams { export type AddEditQueryParams = Partial>; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-add-edit-v2", templateUrl: "add-edit-v2.component.html", diff --git a/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts b/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts index 0b7346c8613..b314c48fecd 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts @@ -28,6 +28,8 @@ import { PopupFooterComponent } from "../../../../../platform/popup/layout/popup import { PopupHeaderComponent } from "../../../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../../../platform/popup/layout/popup-page.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-assign-collections", templateUrl: "./assign-collections.component.html", diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.spec.ts index 6e4215c1ec2..871163ac80b 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.spec.ts @@ -25,20 +25,30 @@ import { PopupRouterCacheService } from "../../../../../platform/popup/view-cach import { AttachmentsV2Component } from "./attachments-v2.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "popup-header", template: ``, }) class MockPopupHeaderComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() pageTitle: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() backAction: () => void; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "popup-footer", template: ``, }) class MockPopupFooterComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() pageTitle: string; } diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.ts index fc6d882dfd5..295496c701f 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.ts @@ -17,6 +17,8 @@ import { PopupHeaderComponent } from "../../../../../platform/popup/layout/popup import { PopupPageComponent } from "../../../../../platform/popup/layout/popup-page.component"; import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-attachments-v2", templateUrl: "./attachments-v2.component.html", diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts index 26410a46187..e2af3c44c7e 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts @@ -25,6 +25,8 @@ import { CipherFormContainer } from "@bitwarden/vault"; import BrowserPopupUtils from "../../../../../../platform/browser/browser-popup-utils"; import { FilePopoutUtilsService } from "../../../../../../tools/popup/services/file-popout-utils.service"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-open-attachments", templateUrl: "./open-attachments.component.html", @@ -39,6 +41,8 @@ import { FilePopoutUtilsService } from "../../../../../../tools/popup/services/f }) export class OpenAttachmentsComponent implements OnInit { /** Cipher `id` */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) cipherId: CipherId; /** True when the attachments window should be opened in a popout */ diff --git a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.ts b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.ts index 1eef907821d..64f662ab840 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.ts @@ -15,6 +15,8 @@ import { VaultPopupItemsService } from "../../../services/vault-popup-items.serv import { PopupCipherViewLike } from "../../../views/popup-cipher.view"; import { VaultListItemsContainerComponent } from "../vault-list-items-container/vault-list-items-container.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ imports: [ CommonModule, @@ -46,7 +48,7 @@ export class AutofillVaultListItemsComponent { startWith(true), // Start with true to avoid flashing the fill button on first load ); - protected groupByType = toSignal( + protected readonly groupByType = toSignal( this.vaultPopupItemsService.hasFilterApplied$.pipe(map((hasFilter) => !hasFilter)), ); diff --git a/apps/browser/src/vault/popup/components/vault-v2/blocked-injection-banner/blocked-injection-banner.component.ts b/apps/browser/src/vault/popup/components/vault-v2/blocked-injection-banner/blocked-injection-banner.component.ts index 5824e8d97ea..2125af289a2 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/blocked-injection-banner/blocked-injection-banner.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/blocked-injection-banner/blocked-injection-banner.component.ts @@ -15,6 +15,8 @@ import { VaultPopupAutofillService } from "../../../services/vault-popup-autofil const blockedURISettingsRoute = "/blocked-domains"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ imports: [ BannerModule, diff --git a/apps/browser/src/vault/popup/components/vault-v2/intro-carousel/intro-carousel.component.ts b/apps/browser/src/vault/popup/components/vault-v2/intro-carousel/intro-carousel.component.ts index 94996a054e6..48c8f5682bc 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/intro-carousel/intro-carousel.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/intro-carousel/intro-carousel.component.ts @@ -9,6 +9,8 @@ import { VaultCarouselModule } from "@bitwarden/vault"; import { IntroCarouselService } from "../../../services/intro-carousel.service"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-intro-carousel", templateUrl: "./intro-carousel.component.html", diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.ts b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.ts index 6c7e8bcfbc3..2e2ee5cd56b 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.ts @@ -21,6 +21,8 @@ type CipherItem = { field: CopyAction; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-item-copy-actions", templateUrl: "item-copy-actions.component.html", @@ -35,6 +37,8 @@ type CipherItem = { }) export class ItemCopyActionsComponent { protected showQuickCopyActions$ = inject(VaultPopupCopyButtonsService).showQuickCopyActions$; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) cipher!: CipherViewLike; protected CipherViewLikeUtils = CipherViewLikeUtils; diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts index 1b8403e6024..94016d2670f 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts @@ -34,6 +34,8 @@ import { PasswordRepromptService } from "@bitwarden/vault"; import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service"; import { AddEditQueryParams } from "../add-edit/add-edit-v2.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-item-more-options", templateUrl: "./item-more-options.component.html", @@ -42,6 +44,8 @@ import { AddEditQueryParams } from "../add-edit/add-edit-v2.component"; export class ItemMoreOptionsComponent { private _cipher$ = new BehaviorSubject(undefined); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true, }) @@ -57,6 +61,8 @@ export class ItemMoreOptionsComponent { * Flag to show view item menu option. Used when something else is * assigned as the primary action for the item, such as autofill. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ transform: booleanAttribute }) showViewOption: boolean; @@ -64,6 +70,8 @@ export class ItemMoreOptionsComponent { * Flag to hide the autofill menu options. Used for items that are * already in the autofill list suggestion. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ transform: booleanAttribute }) hideAutofillOptions: boolean; diff --git a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts index d1586bd6ad5..004980db181 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts @@ -23,6 +23,8 @@ export interface NewItemInitialValues { collectionId?: CollectionId; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-new-item-dropdown", templateUrl: "new-item-dropdown-v2.component.html", @@ -34,6 +36,8 @@ export class NewItemDropdownV2Component implements OnInit { /** * Optional initial values to pass to the add cipher form */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() initialValues: NewItemInitialValues; diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.spec.ts index b65138dac3a..2139b6d9a4f 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.spec.ts @@ -18,14 +18,24 @@ import { VaultGeneratorDialogComponent, } from "./vault-generator-dialog.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-cipher-form-generator", template: "", }) class MockCipherFormGenerator { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() type: "password" | "username" = "password"; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() algorithmSelected: EventEmitter = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() uri: string = ""; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() valueGenerated = new EventEmitter(); } diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.ts index b0103aaacfb..caeebdabc09 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.ts @@ -38,6 +38,8 @@ export const GeneratorDialogAction = { type GeneratorDialogAction = UnionOfValues; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-vault-generator-dialog", templateUrl: "./vault-generator-dialog.component.html", diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.ts index f64b5e6b83d..6381b8be147 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.ts @@ -17,6 +17,8 @@ import { VaultPopupListFiltersService } from "../../../../../vault/popup/service import { VaultListFiltersComponent } from "../vault-list-filters/vault-list-filters.component"; import { VaultV2SearchComponent } from "../vault-search/vault-v2-search.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-vault-header-v2", templateUrl: "vault-header-v2.component.html", @@ -31,6 +33,8 @@ import { VaultV2SearchComponent } from "../vault-search/vault-v2-search.componen ], }) export class VaultHeaderV2Component { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild(DisclosureComponent) disclosure: DisclosureComponent; /** Emits the visibility status of the disclosure component. */ diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-filters/vault-list-filters.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-list-filters/vault-list-filters.component.ts index 81fad896ad2..50da66fe5b8 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-filters/vault-list-filters.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-filters/vault-list-filters.component.ts @@ -8,6 +8,8 @@ import { ChipSelectComponent } from "@bitwarden/components"; import { VaultPopupListFiltersService } from "../../../services/vault-popup-list-filters.service"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-vault-list-filters", templateUrl: "./vault-list-filters.component.html", diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts index 61d7815d93e..6850a474af5 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts @@ -90,12 +90,18 @@ export class VaultListItemsContainerComponent implements AfterViewInit { private vaultPopupSectionService = inject(VaultPopupSectionService); protected CipherViewLikeUtils = CipherViewLikeUtils; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild(CdkVirtualScrollViewport, { static: false }) viewPort!: CdkVirtualScrollViewport; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild(DisclosureComponent) disclosure!: DisclosureComponent; /** * Indicates whether the section should be open or closed if collapsibleKey is provided */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals protected sectionOpenState: Signal = computed(() => { if (!this.collapsibleKey()) { return true; @@ -130,17 +136,23 @@ export class VaultListItemsContainerComponent implements AfterViewInit { */ private viewCipherTimeout?: number; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals ciphers = input([]); /** * If true, we will group ciphers by type (Login, Card, Identity) * within subheadings in a single container, converted to a WritableSignal. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals groupByType = input(false); /** * Computed signal for a grouped list of ciphers with an optional header */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals cipherGroups = computed< { subHeaderKey?: string; @@ -183,6 +195,8 @@ export class VaultListItemsContainerComponent implements AfterViewInit { /** * Title for the vault list item section. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals title = input(undefined); /** @@ -191,33 +205,45 @@ export class VaultListItemsContainerComponent implements AfterViewInit { * The key must be added to the state definition in `vault-popup-section.service.ts` since the * collapsed state is stored locally. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals collapsibleKey = input(undefined); /** * Optional description for the vault list item section. Will be shown below the title even when * no ciphers are available. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals description = input(undefined); /** * Option to show a refresh button in the section header. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals showRefresh = input(false, { transform: booleanAttribute }); /** * Event emitted when the refresh button is clicked. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onRefresh = new EventEmitter(); /** * Flag indicating that the current tab location is blocked */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals currentURIIsBlocked = toSignal(this.vaultPopupAutofillService.currentTabIsOnBlocklist$); /** * Resolved i18n key to use for suggested cipher items */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals cipherItemTitleKey = computed(() => { return (cipher: CipherViewLike) => { const login = CipherViewLikeUtils.getLogin(cipher); @@ -233,11 +259,15 @@ export class VaultListItemsContainerComponent implements AfterViewInit { /** * Option to show the autofill button for each item. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals showAutofillButton = input(false, { transform: booleanAttribute }); /** * Flag indicating whether the suggested cipher item autofill button should be shown or not */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals hideAutofillButton = computed( () => !this.showAutofillButton() || this.currentURIIsBlocked() || this.primaryActionAutofill(), ); @@ -245,22 +275,30 @@ export class VaultListItemsContainerComponent implements AfterViewInit { /** * Flag indicating whether the cipher item autofill menu options should be shown or not */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals hideAutofillMenuOptions = computed(() => this.currentURIIsBlocked() || this.showAutofillButton()); /** * Option to perform autofill operation as the primary action for autofill suggestions. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals primaryActionAutofill = input(false, { transform: booleanAttribute }); /** * Remove the bottom margin from the bit-section in this component * (used for containers at the end of the page where bottom margin is not needed) */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals disableSectionMargin = input(false, { transform: booleanAttribute }); /** * Remove the description margin */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals disableDescriptionMargin = input(false, { transform: booleanAttribute }); /** @@ -275,6 +313,8 @@ export class VaultListItemsContainerComponent implements AfterViewInit { return collections[0]?.name; } + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals protected autofillShortcutTooltip = signal(undefined); constructor( diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts index f2764df7ba7..7b9f358c01c 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts @@ -18,6 +18,8 @@ import { PopupHeaderComponent } from "../../../../../platform/popup/layout/popup import { PopupPageComponent } from "../../../../../platform/popup/layout/popup-page.component"; import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-password-history-v2", templateUrl: "vault-password-history-v2.component.html", diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-search/vault-v2-search.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-search/vault-v2-search.component.ts index 72df3cba41a..c254c290915 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-search/vault-v2-search.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-search/vault-v2-search.component.ts @@ -10,6 +10,8 @@ import { SearchModule } from "@bitwarden/components"; import { VaultPopupItemsService } from "../../../services/vault-popup-items.service"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ imports: [CommonModule, SearchModule, JslibModule, FormsModule], selector: "app-vault-v2-search", diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts index 604cc6b73ef..2dd6c1a0ce1 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts @@ -64,6 +64,8 @@ const VaultState = { type VaultState = UnionOfValues; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-vault", templateUrl: "vault-v2.component.html", @@ -89,6 +91,8 @@ type VaultState = UnionOfValues; ], }) export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild(CdkVirtualScrollableElement) virtualScrollElement?: CdkVirtualScrollableElement; NudgeType = NudgeType; diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts index 915a27e4fd1..30074777e83 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts @@ -76,6 +76,8 @@ type LoadAction = | typeof COPY_VERIFICATION_CODE_ID | typeof UPDATE_PASSWORD; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-view-v2", templateUrl: "view-v2.component.html", diff --git a/apps/browser/src/vault/popup/services/vault-popup-section.service.ts b/apps/browser/src/vault/popup/services/vault-popup-section.service.ts index ed641e0cdf7..b93eda72506 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-section.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-section.service.ts @@ -31,7 +31,7 @@ export class VaultPopupSectionService { private vaultPopupItemsService = inject(VaultPopupItemsService); private stateProvider = inject(StateProvider); - private hasFilterOrSearchApplied = toSignal( + private readonly hasFilterOrSearchApplied = toSignal( this.vaultPopupItemsService.hasFilterApplied$.pipe(map((hasFilter) => hasFilter)), ); @@ -40,7 +40,7 @@ export class VaultPopupSectionService { * application-applied overrides. * `null` means there is no current override */ - private temporaryStateOverride = signal | null>(null); + private readonly temporaryStateOverride = signal | null>(null); constructor() { effect( @@ -71,7 +71,7 @@ export class VaultPopupSectionService { * Stored disk state for the open/close state of the sections, with an initial value provided * if the stored disk state does not yet exist. */ - private sectionOpenStoredState = toSignal( + private readonly sectionOpenStoredState = toSignal( this.sectionOpenStateProvider.state$.pipe(map((sectionOpen) => sectionOpen ?? INITIAL_OPEN)), // Indicates that the state value is loading { initialValue: null }, @@ -81,7 +81,7 @@ export class VaultPopupSectionService { * Indicates the current open/close display state of each section, accounting for temporary * non-persisted overrides. */ - sectionOpenDisplayState: Signal> = computed(() => ({ + readonly sectionOpenDisplayState: Signal> = computed(() => ({ ...this.sectionOpenStoredState(), ...this.temporaryStateOverride(), })); diff --git a/apps/browser/src/vault/popup/settings/appearance-v2.component.spec.ts b/apps/browser/src/vault/popup/settings/appearance-v2.component.spec.ts index 738ec3ae1ff..9e1beab5787 100644 --- a/apps/browser/src/vault/popup/settings/appearance-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/settings/appearance-v2.component.spec.ts @@ -22,20 +22,30 @@ import { VaultPopupCopyButtonsService } from "../services/vault-popup-copy-butto import { AppearanceV2Component } from "./appearance-v2.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "popup-header", template: ``, }) class MockPopupHeaderComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() pageTitle: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() backAction: () => void; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "popup-page", template: ``, }) class MockPopupPageComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() loading: boolean; } diff --git a/apps/browser/src/vault/popup/settings/appearance-v2.component.ts b/apps/browser/src/vault/popup/settings/appearance-v2.component.ts index 23a609bd008..e6515ae7461 100644 --- a/apps/browser/src/vault/popup/settings/appearance-v2.component.ts +++ b/apps/browser/src/vault/popup/settings/appearance-v2.component.ts @@ -33,6 +33,8 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co import { PopupSizeService } from "../../../platform/popup/layout/popup-size.service"; import { VaultPopupCopyButtonsService } from "../services/vault-popup-copy-buttons.service"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "./appearance-v2.component.html", imports: [ diff --git a/apps/browser/src/vault/popup/settings/archive.component.ts b/apps/browser/src/vault/popup/settings/archive.component.ts index 2044389f295..58925eda428 100644 --- a/apps/browser/src/vault/popup/settings/archive.component.ts +++ b/apps/browser/src/vault/popup/settings/archive.component.ts @@ -33,6 +33,8 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "archive.component.html", standalone: true, diff --git a/apps/browser/src/vault/popup/settings/download-bitwarden.component.ts b/apps/browser/src/vault/popup/settings/download-bitwarden.component.ts index d23d00a1ad7..109f3ea0404 100644 --- a/apps/browser/src/vault/popup/settings/download-bitwarden.component.ts +++ b/apps/browser/src/vault/popup/settings/download-bitwarden.component.ts @@ -13,6 +13,8 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "download-bitwarden.component.html", imports: [ diff --git a/apps/browser/src/vault/popup/settings/folders-v2.component.spec.ts b/apps/browser/src/vault/popup/settings/folders-v2.component.spec.ts index d1450667fa8..3cb5503ed89 100644 --- a/apps/browser/src/vault/popup/settings/folders-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/settings/folders-v2.component.spec.ts @@ -21,20 +21,30 @@ import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-heade import { FoldersV2Component } from "./folders-v2.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "popup-header", template: ``, }) class MockPopupHeaderComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() pageTitle: string = ""; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() backAction: () => void = () => {}; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "popup-footer", template: ``, }) class MockPopupFooterComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() pageTitle: string = ""; } diff --git a/apps/browser/src/vault/popup/settings/folders-v2.component.ts b/apps/browser/src/vault/popup/settings/folders-v2.component.ts index b749f651d53..20a816e7297 100644 --- a/apps/browser/src/vault/popup/settings/folders-v2.component.ts +++ b/apps/browser/src/vault/popup/settings/folders-v2.component.ts @@ -22,6 +22,8 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "./folders-v2.component.html", imports: [ diff --git a/apps/browser/src/vault/popup/settings/more-from-bitwarden-page-v2.component.ts b/apps/browser/src/vault/popup/settings/more-from-bitwarden-page-v2.component.ts index ec7a73a3bc3..2f9fae43da7 100644 --- a/apps/browser/src/vault/popup/settings/more-from-bitwarden-page-v2.component.ts +++ b/apps/browser/src/vault/popup/settings/more-from-bitwarden-page-v2.component.ts @@ -17,6 +17,8 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "more-from-bitwarden-page-v2.component.html", imports: [ diff --git a/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts index 1676fea3c01..70ba6842a0d 100644 --- a/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts +++ b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts @@ -53,9 +53,13 @@ export class TrashListItemsContainerComponent { /** * The list of trashed items to display. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() ciphers: PopupCipherViewLike[] = []; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() headerText: string; diff --git a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.ts b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.ts index 92cbf951ead..ff6e9b4065c 100644 --- a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.ts +++ b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.ts @@ -19,6 +19,8 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "vault-settings-v2.component.html", imports: [ @@ -37,12 +39,12 @@ export class VaultSettingsV2Component implements OnInit, OnDestroy { private userId$ = this.accountService.activeAccount$.pipe(getUserId); // Check if user is premium user, they will be able to archive items - protected userCanArchive = toSignal( + protected readonly userCanArchive = toSignal( this.userId$.pipe(switchMap((userId) => this.cipherArchiveService.userCanArchive$(userId))), ); // Check if user has archived items (does not check if user is premium) - protected showArchiveFilter = toSignal( + protected readonly showArchiveFilter = toSignal( this.userId$.pipe(switchMap((userId) => this.cipherArchiveService.showArchiveVault$(userId))), ); diff --git a/apps/desktop/src/vault/app/vault/assign-collections/assign-collections-desktop.component.ts b/apps/desktop/src/vault/app/vault/assign-collections/assign-collections-desktop.component.ts index d81f1662c6c..5af1f96a569 100644 --- a/apps/desktop/src/vault/app/vault/assign-collections/assign-collections-desktop.component.ts +++ b/apps/desktop/src/vault/app/vault/assign-collections/assign-collections-desktop.component.ts @@ -10,6 +10,8 @@ import { CollectionAssignmentResult, } from "@bitwarden/vault"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ standalone: true, templateUrl: "./assign-collections-desktop.component.html", diff --git a/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.ts b/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.ts index 26349920106..775ef55b3eb 100644 --- a/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.ts +++ b/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.ts @@ -39,6 +39,8 @@ export const CredentialGeneratorDialogAction = { type CredentialGeneratorDialogAction = UnionOfValues; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "credential-generator-dialog", templateUrl: "credential-generator-dialog.component.html", diff --git a/apps/desktop/src/vault/app/vault/item-footer.component.ts b/apps/desktop/src/vault/app/vault/item-footer.component.ts index 5ebd657cee0..0034bd9a43c 100644 --- a/apps/desktop/src/vault/app/vault/item-footer.component.ts +++ b/apps/desktop/src/vault/app/vault/item-footer.component.ts @@ -25,22 +25,46 @@ import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cip import { ButtonComponent, ButtonModule, DialogService, ToastService } from "@bitwarden/components"; import { ArchiveCipherUtilitiesService, PasswordRepromptService } from "@bitwarden/vault"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-vault-item-footer", templateUrl: "item-footer.component.html", imports: [ButtonModule, CommonModule, JslibModule], }) export class ItemFooterComponent implements OnInit, OnChanges { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) cipher: CipherView = new CipherView(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() collectionId: string | null = null; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) action: string = "view"; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() masterPasswordAlreadyPrompted: boolean = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onEdit = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onClone = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onDelete = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onRestore = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onCancel = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onArchiveToggle = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("submitBtn", { static: false }) submitBtn: ButtonComponent | null = null; activeUserId: UserId | null = null; diff --git a/apps/desktop/src/vault/app/vault/vault-filter/filters/collection-filter.component.ts b/apps/desktop/src/vault/app/vault/vault-filter/filters/collection-filter.component.ts index 22372410e5b..015b301efdb 100644 --- a/apps/desktop/src/vault/app/vault/vault-filter/filters/collection-filter.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-filter/filters/collection-filter.component.ts @@ -2,6 +2,8 @@ import { Component } from "@angular/core"; import { CollectionFilterComponent as BaseCollectionFilterComponent } from "@bitwarden/angular/vault/vault-filter/components/collection-filter.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-collection-filter", templateUrl: "collection-filter.component.html", diff --git a/apps/desktop/src/vault/app/vault/vault-filter/filters/folder-filter.component.ts b/apps/desktop/src/vault/app/vault/vault-filter/filters/folder-filter.component.ts index d7364808f6d..f340e4082b8 100644 --- a/apps/desktop/src/vault/app/vault/vault-filter/filters/folder-filter.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-filter/filters/folder-filter.component.ts @@ -2,6 +2,8 @@ import { Component } from "@angular/core"; import { FolderFilterComponent as BaseFolderFilterComponent } from "@bitwarden/angular/vault/vault-filter/components/folder-filter.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-folder-filter", templateUrl: "folder-filter.component.html", diff --git a/apps/desktop/src/vault/app/vault/vault-filter/filters/organization-filter.component.ts b/apps/desktop/src/vault/app/vault/vault-filter/filters/organization-filter.component.ts index 503c2b2ec6e..99338ddbb7c 100644 --- a/apps/desktop/src/vault/app/vault/vault-filter/filters/organization-filter.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-filter/filters/organization-filter.component.ts @@ -9,6 +9,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ToastService } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-organization-filter", templateUrl: "organization-filter.component.html", diff --git a/apps/desktop/src/vault/app/vault/vault-filter/filters/status-filter.component.ts b/apps/desktop/src/vault/app/vault/vault-filter/filters/status-filter.component.ts index 276b11d7138..db546f76a2c 100644 --- a/apps/desktop/src/vault/app/vault/vault-filter/filters/status-filter.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-filter/filters/status-filter.component.ts @@ -2,6 +2,8 @@ import { Component } from "@angular/core"; import { StatusFilterComponent as BaseStatusFilterComponent } from "@bitwarden/angular/vault/vault-filter/components/status-filter.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-status-filter", templateUrl: "status-filter.component.html", diff --git a/apps/desktop/src/vault/app/vault/vault-filter/filters/type-filter.component.ts b/apps/desktop/src/vault/app/vault/vault-filter/filters/type-filter.component.ts index 27e7d5c5ecb..fbab7ce4667 100644 --- a/apps/desktop/src/vault/app/vault/vault-filter/filters/type-filter.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-filter/filters/type-filter.component.ts @@ -5,6 +5,8 @@ import { TypeFilterComponent as BaseTypeFilterComponent } from "@bitwarden/angul import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-type-filter", templateUrl: "type-filter.component.html", diff --git a/apps/desktop/src/vault/app/vault/vault-filter/vault-filter.component.ts b/apps/desktop/src/vault/app/vault/vault-filter/vault-filter.component.ts index 161d22687e8..d7c5bafc3a4 100644 --- a/apps/desktop/src/vault/app/vault/vault-filter/vault-filter.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-filter/vault-filter.component.ts @@ -2,6 +2,8 @@ import { Component } from "@angular/core"; import { VaultFilterComponent as BaseVaultFilterComponent } from "@bitwarden/angular/vault/vault-filter/components/vault-filter.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-vault-filter", templateUrl: "vault-filter.component.html", diff --git a/apps/desktop/src/vault/app/vault/vault-items-v2.component.ts b/apps/desktop/src/vault/app/vault/vault-items-v2.component.ts index 290a38ac08c..d312d49277a 100644 --- a/apps/desktop/src/vault/app/vault/vault-items-v2.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-items-v2.component.ts @@ -21,6 +21,8 @@ import { MenuModule } from "@bitwarden/components"; import { SearchBarService } from "../../../app/layout/search/search-bar.service"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-vault-items-v2", templateUrl: "vault-items-v2.component.html", diff --git a/apps/desktop/src/vault/app/vault/vault-v2.component.ts b/apps/desktop/src/vault/app/vault/vault-v2.component.ts index b7b0bf2e1b2..19c9cffeeb2 100644 --- a/apps/desktop/src/vault/app/vault/vault-v2.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-v2.component.ts @@ -94,6 +94,8 @@ import { VaultItemsV2Component } from "./vault-items-v2.component"; const BroadcasterSubscriptionId = "VaultComponent"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-vault", templateUrl: "vault-v2.component.html", @@ -138,12 +140,20 @@ const BroadcasterSubscriptionId = "VaultComponent"; export class VaultV2Component implements OnInit, OnDestroy, CopyClickListener { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild(VaultItemsV2Component, { static: true }) vaultItemsComponent: VaultItemsV2Component | null = null; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild(VaultFilterComponent, { static: true }) vaultFilterComponent: VaultFilterComponent | null = null; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("folderAddEdit", { read: ViewContainerRef, static: true }) folderAddEditModalRef: ViewContainerRef | null = null; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild(CipherFormComponent) cipherFormComponent: CipherFormComponent | null = null; diff --git a/apps/web/src/app/vault/components/assign-collections/assign-collections-web.component.ts b/apps/web/src/app/vault/components/assign-collections/assign-collections-web.component.ts index 753d2708e60..2b97222fb14 100644 --- a/apps/web/src/app/vault/components/assign-collections/assign-collections-web.component.ts +++ b/apps/web/src/app/vault/components/assign-collections/assign-collections-web.component.ts @@ -12,6 +12,8 @@ import { import { SharedModule } from "../../../shared"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ imports: [SharedModule, AssignCollectionsComponent, PluralizePipe], templateUrl: "./assign-collections-web.component.html", diff --git a/apps/web/src/app/vault/components/browser-extension-prompt/browser-extension-prompt-install.component.ts b/apps/web/src/app/vault/components/browser-extension-prompt/browser-extension-prompt-install.component.ts index 005fbb1b14d..2444ed1f707 100644 --- a/apps/web/src/app/vault/components/browser-extension-prompt/browser-extension-prompt-install.component.ts +++ b/apps/web/src/app/vault/components/browser-extension-prompt/browser-extension-prompt-install.component.ts @@ -25,6 +25,8 @@ const WebStoreUrls: Partial> = { "https://microsoftedge.microsoft.com/addons/detail/jbkfoedolllekgbhcbcoahefnbanhhlh", }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-browser-extension-prompt-install", templateUrl: "./browser-extension-prompt-install.component.html", diff --git a/apps/web/src/app/vault/components/browser-extension-prompt/browser-extension-prompt.component.ts b/apps/web/src/app/vault/components/browser-extension-prompt/browser-extension-prompt.component.ts index f3a5b9aa532..cb927d0848c 100644 --- a/apps/web/src/app/vault/components/browser-extension-prompt/browser-extension-prompt.component.ts +++ b/apps/web/src/app/vault/components/browser-extension-prompt/browser-extension-prompt.component.ts @@ -14,6 +14,8 @@ import { } from "../../services/browser-extension-prompt.service"; import { ManuallyOpenExtensionComponent } from "../manually-open-extension/manually-open-extension.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-browser-extension-prompt", templateUrl: "./browser-extension-prompt.component.html", diff --git a/apps/web/src/app/vault/components/manually-open-extension/manually-open-extension.component.ts b/apps/web/src/app/vault/components/manually-open-extension/manually-open-extension.component.ts index 646ff76311e..6105aeacf9c 100644 --- a/apps/web/src/app/vault/components/manually-open-extension/manually-open-extension.component.ts +++ b/apps/web/src/app/vault/components/manually-open-extension/manually-open-extension.component.ts @@ -4,6 +4,8 @@ import { BitwardenIcon } from "@bitwarden/assets/svg"; import { IconModule } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-manually-open-extension", templateUrl: "./manually-open-extension.component.html", diff --git a/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.ts b/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.ts index 5f4e3f586f5..9237d70b996 100644 --- a/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.ts +++ b/apps/web/src/app/vault/components/setup-extension/add-extension-later-dialog.component.ts @@ -16,6 +16,8 @@ export type AddExtensionLaterDialogData = { onDismiss: () => void; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-add-extension-later-dialog", templateUrl: "./add-extension-later-dialog.component.html", diff --git a/apps/web/src/app/vault/components/setup-extension/add-extension-videos.component.ts b/apps/web/src/app/vault/components/setup-extension/add-extension-videos.component.ts index c9c222e8e64..9a974a395f0 100644 --- a/apps/web/src/app/vault/components/setup-extension/add-extension-videos.component.ts +++ b/apps/web/src/app/vault/components/setup-extension/add-extension-videos.component.ts @@ -6,12 +6,16 @@ import { debounceTime, fromEvent } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { DarkImageSourceDirective } from "@bitwarden/vault"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-add-extension-videos", templateUrl: "./add-extension-videos.component.html", imports: [CommonModule, JslibModule, DarkImageSourceDirective], }) export class AddExtensionVideosComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChildren("video", { read: ElementRef }) protected videoElements!: QueryList< ElementRef >; diff --git a/apps/web/src/app/vault/components/setup-extension/setup-extension.component.ts b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.ts index 012ac370c70..b5c0d096944 100644 --- a/apps/web/src/app/vault/components/setup-extension/setup-extension.component.ts +++ b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.ts @@ -42,6 +42,8 @@ export const SetupExtensionState = { type SetupExtensionState = UnionOfValues; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-setup-extension", templateUrl: "./setup-extension.component.html", diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts index b48db2bba91..98922fb114f 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts @@ -129,6 +129,8 @@ export const VaultItemDialogResult = { export type VaultItemDialogResult = UnionOfValues; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-vault-item-dialog", templateUrl: "vault-item-dialog.component.html", @@ -159,9 +161,13 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { * Reference to the dialog content element. Used to scroll to the top of the dialog when switching modes. * @protected */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("dialogContent") protected dialogContent: ElementRef; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild(CipherFormComponent) cipherFormComponent!: CipherFormComponent; /** diff --git a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.ts b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.ts index 1e4b33777bb..4883043ddd6 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.ts @@ -28,6 +28,8 @@ import { import { VaultItemEvent } from "./vault-item-event"; import { RowHeightClass } from "./vault-items.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "tr[appVaultCipherRow]", templateUrl: "vault-cipher-row.component.html", @@ -36,42 +38,86 @@ import { RowHeightClass } from "./vault-items.component"; export class VaultCipherRowComponent implements OnInit { protected RowHeightClass = RowHeightClass; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild(MenuTriggerForDirective, { static: false }) menuTrigger: MenuTriggerForDirective; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() disabled: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() cipher: C; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() showOwner: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() showCollections: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() showGroups: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() showPremiumFeatures: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() useEvents: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() cloneable: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() organizations: Organization[]; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() collections: CollectionView[]; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() viewingOrgVault: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() canEditCipher: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() canAssignCollections: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() canManageCollection: boolean; /** * uses new permission delete logic from PM-15493 */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() canDeleteCipher: boolean; /** * uses new permission restore logic from PM-15493 */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() canRestoreCipher: boolean; /** * user has archive permissions */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() userCanArchive: boolean; /** * Enforge Org Data Ownership Policy Status */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() enforceOrgDataOwnershipPolicy: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onEvent = new EventEmitter>(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() checked: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() checkedToggled = new EventEmitter(); protected CipherType = CipherType; diff --git a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts index 746024eced8..daa981d509a 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts @@ -23,6 +23,8 @@ import { import { VaultItemEvent } from "./vault-item-event"; import { RowHeightClass } from "./vault-items.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "tr[appVaultCollectionRow]", templateUrl: "vault-collection-row.component.html", @@ -34,23 +36,53 @@ export class VaultCollectionRowComponent { protected CollectionPermission = CollectionPermission; protected DefaultCollectionType = CollectionTypes.DefaultUserCollection; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild(MenuTriggerForDirective, { static: false }) menuTrigger: MenuTriggerForDirective; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() disabled: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() collection: CollectionView; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() showOwner: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() showCollections: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() showGroups: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() canEditCollection: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() canDeleteCollection: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() canViewCollectionInfo: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() organizations: Organization[]; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() groups: GroupView[]; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() showPermissionsColumn: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onEvent = new EventEmitter>(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() checked: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() checkedToggled = new EventEmitter(); constructor(private i18nService: I18nService) {} diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts index 81ad29db9dd..9ea4c209009 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts @@ -36,6 +36,8 @@ const MaxSelectionCount = 500; type ItemPermission = CollectionPermission | "NoAccess"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-vault-items", templateUrl: "vault-items.component.html", @@ -44,32 +46,76 @@ type ItemPermission = CollectionPermission | "NoAccess"; export class VaultItemsComponent { protected RowHeight = RowHeight; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() disabled: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() showOwner: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() showCollections: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() showGroups: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() useEvents: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() showPremiumFeatures: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() showBulkMove: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() showBulkTrashOptions: boolean; // Encompasses functionality only available from the organization vault context + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() showAdminActions = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() allOrganizations: Organization[] = []; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() allCollections: CollectionView[] = []; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() allGroups: GroupView[] = []; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() showBulkEditCollectionAccess = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() showBulkAddToCollections = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() showPermissionsColumn = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() viewingOrgVault: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() addAccessStatus: number; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() addAccessToggle: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() activeCollection: CollectionView | undefined; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() userCanArchive: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() enforceOrgDataOwnershipPolicy: boolean; private readonly restrictedPolicies = toSignal(this.restrictedItemTypesService.restricted$); private _ciphers?: C[] = []; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() get ciphers(): C[] { return this._ciphers; } @@ -79,6 +125,8 @@ export class VaultItemsComponent { } private _collections?: CollectionView[] = []; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() get collections(): CollectionView[] { return this._collections; } @@ -87,6 +135,8 @@ export class VaultItemsComponent { this.refreshItems(); } + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onEvent = new EventEmitter>(); protected editableItems: VaultItem[] = []; diff --git a/apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.spec.ts b/apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.spec.ts index afb32738901..c2d6c87d865 100644 --- a/apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.spec.ts +++ b/apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.spec.ts @@ -16,14 +16,24 @@ import { WebVaultGeneratorDialogResult, } from "./web-generator-dialog.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-cipher-form-generator", template: "", }) class MockCipherFormGenerator { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() type: "password" | "username" = "password"; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() algorithmSelected: EventEmitter = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() uri?: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() valueGenerated = new EventEmitter(); } diff --git a/apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.ts b/apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.ts index 7454b4d10f0..957f72015a5 100644 --- a/apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.ts +++ b/apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.ts @@ -34,6 +34,8 @@ export const WebVaultGeneratorDialogAction = { type WebVaultGeneratorDialogAction = UnionOfValues; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "web-vault-generator-dialog", templateUrl: "./web-generator-dialog.component.html", diff --git a/apps/web/src/app/vault/individual-vault/add-edit-v2.component.ts b/apps/web/src/app/vault/individual-vault/add-edit-v2.component.ts index c09238e7953..41c922cf4fe 100644 --- a/apps/web/src/app/vault/individual-vault/add-edit-v2.component.ts +++ b/apps/web/src/app/vault/individual-vault/add-edit-v2.component.ts @@ -62,6 +62,8 @@ export interface AddEditCipherDialogCloseResult { * Component for viewing a cipher, presented in a dialog. * @deprecated Use the VaultItemDialogComponent instead. */ +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-vault-add-edit-v2", templateUrl: "add-edit-v2.component.html", diff --git a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts index 78abad1ebf8..3856bb65324 100644 --- a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts +++ b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts @@ -52,6 +52,8 @@ export const openBulkDeleteDialog = ( ); }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "bulk-delete-dialog.component.html", standalone: false, diff --git a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts index ef43a3ead81..f76e505f87d 100644 --- a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts +++ b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts @@ -46,6 +46,8 @@ export const openBulkMoveDialog = ( ); }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "bulk-move-dialog.component.html", standalone: false, diff --git a/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.ts b/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.ts index 79fae4d5b1f..19c462193e1 100644 --- a/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.ts +++ b/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.ts @@ -10,14 +10,22 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { Utils } from "@bitwarden/common/platform/misc/utils"; import { OrganizationId } from "@bitwarden/sdk-internal"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-org-badge", templateUrl: "organization-name-badge.component.html", standalone: false, }) export class OrganizationNameBadgeComponent implements OnChanges { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() organizationId?: OrganizationId | string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() organizationName: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() disabled: boolean; // Need a separate variable or we get weird behavior when used as part of cdk virtual scrolling diff --git a/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.ts b/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.ts index 78624b3662c..80626d258f8 100644 --- a/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-banners/vault-banners.component.ts @@ -17,6 +17,8 @@ import { SharedModule } from "../../../shared"; import { VaultBannersService, VisibleVaultBanner } from "./services/vault-banners.service"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-vault-banners", templateUrl: "./vault-banners.component.html", @@ -32,6 +34,8 @@ export class VaultBannersComponent implements OnInit { visibleBanners: VisibleVaultBanner[] = []; premiumBannerVisible$: Observable; VisibleVaultBanner = VisibleVaultBanner; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() organizations: Organization[] = []; private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id)); diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts index fe5ef281b2d..981e5703cb3 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts @@ -40,6 +40,8 @@ import { LinkSsoService } from "../../../../auth/core/services"; import { OptionsInput } from "../shared/components/vault-filter-section.component"; import { OrganizationFilter } from "../shared/models/vault-filter.type"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-organization-options", templateUrl: "organization-options.component.html", diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts index 180152f054c..0326f8455a6 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts @@ -44,6 +44,8 @@ import { import { OrganizationOptionsComponent } from "./organization-options.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-vault-filter", templateUrl: "vault-filter.component.html", @@ -51,10 +53,18 @@ import { OrganizationOptionsComponent } from "./organization-options.component"; }) export class VaultFilterComponent implements OnInit, OnDestroy { filters?: VaultFilterList; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() activeFilter: VaultFilter = new VaultFilter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onEditFolder = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() searchText = ""; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() searchTextChanged = new EventEmitter(); isLoaded = false; diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.ts index 1a0a96fa19c..e8cf49c3208 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.ts @@ -13,6 +13,8 @@ import { VaultFilterService } from "../../services/abstractions/vault-filter.ser import { VaultFilterSection, VaultFilterType } from "../models/vault-filter-section.type"; import { VaultFilter } from "../models/vault-filter.model"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-filter-section", templateUrl: "vault-filter-section.component.html", @@ -22,7 +24,11 @@ export class VaultFilterSectionComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); private activeUserId$ = getUserId(this.accountService.activeAccount$); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() activeFilter: VaultFilter; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() section: VaultFilterSection; data: TreeNode; diff --git a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts index 929a8d07881..8fa801f5dc0 100644 --- a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts @@ -60,33 +60,53 @@ export class VaultHeaderComponent { * Boolean to determine the loading state of the header. * Shows a loading spinner if set to true */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() loading: boolean = true; /** Current active filter */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() filter: RoutedVaultFilterModel | undefined; /** All organizations that can be shown */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() organizations: Organization[] = []; /** Currently selected collection */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() collection?: TreeNode; /** Whether 'Collection' option is shown in the 'New' dropdown */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() canCreateCollections: boolean = false; /** Emits an event when the new item button is clicked in the header */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onAddCipher = new EventEmitter(); /** Emits an event when the new collection button is clicked in the 'New' dropdown menu */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onAddCollection = new EventEmitter(); /** Emits an event when the new folder button is clicked in the 'New' dropdown menu */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onAddFolder = new EventEmitter(); /** Emits an event when the edit collection button is clicked in the header */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onEditCollection = new EventEmitter<{ tab: CollectionDialogTabType }>(); /** Emits an event when the delete collection button is clicked in the header */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onDeleteCollection = new EventEmitter(); constructor( diff --git a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts index 8dc442abe2e..503c088c3da 100644 --- a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts @@ -32,6 +32,8 @@ import { OnboardingModule } from "../../../shared/components/onboarding/onboardi import { VaultOnboardingService as VaultOnboardingServiceAbstraction } from "./services/abstraction/vault-onboarding.service"; import { VaultOnboardingService, VaultOnboardingTasks } from "./services/vault-onboarding.service"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ imports: [OnboardingModule, CommonModule, JslibModule, LinkModule], providers: [ @@ -44,8 +46,14 @@ import { VaultOnboardingService, VaultOnboardingTasks } from "./services/vault-o templateUrl: "vault-onboarding.component.html", }) export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() ciphers: CipherViewLike[]; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() orgs: Organization[]; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onAddCipher = new EventEmitter(); extensionUrl: string; 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 7ea1d02110d..b9a3bbfdd19 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -154,6 +154,8 @@ type EmptyStateItem = { type EmptyStateMap = Record; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-vault", templateUrl: "vault.component.html", @@ -173,7 +175,11 @@ type EmptyStateMap = Record; ], }) export class VaultComponent implements OnInit, OnDestroy { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("vaultFilter", { static: true }) filterComponent: VaultFilterComponent; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("vaultItems", { static: false }) vaultItemsComponent: VaultItemsComponent; trashCleanupWarning: string = null; diff --git a/apps/web/src/app/vault/settings/purge-vault.component.ts b/apps/web/src/app/vault/settings/purge-vault.component.ts index 4c58a27adb7..a81c14e9cc4 100644 --- a/apps/web/src/app/vault/settings/purge-vault.component.ts +++ b/apps/web/src/app/vault/settings/purge-vault.component.ts @@ -25,6 +25,8 @@ export interface PurgeVaultDialogData { organizationId: string; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "purge-vault.component.html", imports: [SharedModule, UserVerificationModule], diff --git a/libs/angular/src/vault/components/folder-add-edit.component.ts b/libs/angular/src/vault/components/folder-add-edit.component.ts index acf7511284d..486585b810c 100644 --- a/libs/angular/src/vault/components/folder-add-edit.component.ts +++ b/libs/angular/src/vault/components/folder-add-edit.component.ts @@ -16,8 +16,14 @@ import { KeyService } from "@bitwarden/key-management"; @Directive() export class FolderAddEditComponent implements OnInit { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() folderId: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onSavedFolder = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onDeletedFolder = new EventEmitter(); editMode = false; diff --git a/libs/angular/src/vault/components/icon.component.ts b/libs/angular/src/vault/components/icon.component.ts index ee2b535d716..851cec5656b 100644 --- a/libs/angular/src/vault/components/icon.component.ts +++ b/libs/angular/src/vault/components/icon.component.ts @@ -25,14 +25,14 @@ export class IconComponent { /** * The cipher to display the icon for. */ - cipher = input.required(); + readonly cipher = input.required(); /** * coloredIcon will adjust the size of favicons and the colors of the text icon when user is in the item details view. */ - coloredIcon = input(false); + readonly coloredIcon = input(false); - imageLoaded = signal(false); + readonly imageLoaded = signal(false); protected data$: Observable; diff --git a/libs/angular/src/vault/components/spotlight/spotlight.component.ts b/libs/angular/src/vault/components/spotlight/spotlight.component.ts index 3c64318a900..a912e4ce11b 100644 --- a/libs/angular/src/vault/components/spotlight/spotlight.component.ts +++ b/libs/angular/src/vault/components/spotlight/spotlight.component.ts @@ -4,6 +4,8 @@ import { Component, EventEmitter, Input, Output } from "@angular/core"; import { ButtonModule, IconButtonModule, TypographyModule } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "bit-spotlight", templateUrl: "spotlight.component.html", @@ -11,16 +13,30 @@ import { I18nPipe } from "@bitwarden/ui-common"; }) export class SpotlightComponent { // The title of the component + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) title: string | null = null; // The subtitle of the component + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() subtitle?: string | null = null; // The text to display on the button + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() buttonText?: string; // Wheter the component can be dismissed, if true, the component will not show a close button + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() persistent = false; // Optional icon to display on the button + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() buttonIcon: string | null = null; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onDismiss = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onButtonClick = new EventEmitter(); handleButtonClick(event: MouseEvent): void { diff --git a/libs/angular/src/vault/components/vault-items.component.ts b/libs/angular/src/vault/components/vault-items.component.ts index 414ec1509ed..0254ddabf2b 100644 --- a/libs/angular/src/vault/components/vault-items.component.ts +++ b/libs/angular/src/vault/components/vault-items.component.ts @@ -31,10 +31,20 @@ import { @Directive() export class VaultItemsComponent implements OnDestroy { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() activeCipherId: string = null; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onCipherClicked = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onCipherRightClicked = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onAddCipher = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onAddCipherOptions = new EventEmitter(); loaded = false; diff --git a/libs/angular/src/vault/vault-filter/components/collection-filter.component.ts b/libs/angular/src/vault/vault-filter/components/collection-filter.component.ts index e9a6923c2fb..4d4037a3517 100644 --- a/libs/angular/src/vault/vault-filter/components/collection-filter.component.ts +++ b/libs/angular/src/vault/vault-filter/components/collection-filter.component.ts @@ -13,13 +13,25 @@ import { VaultFilter } from "../models/vault-filter.model"; @Directive() export class CollectionFilterComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() hide = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() collapsedFilterNodes: Set; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() collectionNodes: DynamicTreeNode; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() activeFilter: VaultFilter; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onNodeCollapseStateChange: EventEmitter = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onFilterChange: EventEmitter = new EventEmitter(); DefaultCollectionType = CollectionTypes.DefaultUserCollection; diff --git a/libs/angular/src/vault/vault-filter/components/folder-filter.component.ts b/libs/angular/src/vault/vault-filter/components/folder-filter.component.ts index 45605d583aa..8c47a37b31b 100644 --- a/libs/angular/src/vault/vault-filter/components/folder-filter.component.ts +++ b/libs/angular/src/vault/vault-filter/components/folder-filter.component.ts @@ -11,15 +11,31 @@ import { VaultFilter } from "../models/vault-filter.model"; @Directive() export class FolderFilterComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() hide = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() collapsedFilterNodes: Set; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() folderNodes: DynamicTreeNode; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() activeFilter: VaultFilter; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onNodeCollapseStateChange: EventEmitter = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onFilterChange: EventEmitter = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onAddFolder = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onEditFolder = new EventEmitter(); get folders() { diff --git a/libs/angular/src/vault/vault-filter/components/organization-filter.component.ts b/libs/angular/src/vault/vault-filter/components/organization-filter.component.ts index 45198d2bcc5..46be2df3884 100644 --- a/libs/angular/src/vault/vault-filter/components/organization-filter.component.ts +++ b/libs/angular/src/vault/vault-filter/components/organization-filter.component.ts @@ -11,15 +11,31 @@ import { VaultFilter } from "../models/vault-filter.model"; @Directive() export class OrganizationFilterComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() hide = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() collapsedFilterNodes: Set; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() organizations: Organization[]; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() activeFilter: VaultFilter; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() activeOrganizationDataOwnership: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() activeSingleOrganizationPolicy: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onNodeCollapseStateChange: EventEmitter = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onFilterChange: EventEmitter = new EventEmitter(); get displayMode(): DisplayMode { diff --git a/libs/angular/src/vault/vault-filter/components/status-filter.component.ts b/libs/angular/src/vault/vault-filter/components/status-filter.component.ts index dc6a90f928d..6862019ab4e 100644 --- a/libs/angular/src/vault/vault-filter/components/status-filter.component.ts +++ b/libs/angular/src/vault/vault-filter/components/status-filter.component.ts @@ -7,10 +7,20 @@ import { VaultFilter } from "../models/vault-filter.model"; @Directive() export class StatusFilterComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() hideFavorites = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() hideTrash = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() hideArchive = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onFilterChange: EventEmitter = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() activeFilter: VaultFilter; get show() { diff --git a/libs/angular/src/vault/vault-filter/components/type-filter.component.ts b/libs/angular/src/vault/vault-filter/components/type-filter.component.ts index 84cdf976309..a06be5e4b08 100644 --- a/libs/angular/src/vault/vault-filter/components/type-filter.component.ts +++ b/libs/angular/src/vault/vault-filter/components/type-filter.component.ts @@ -10,13 +10,25 @@ import { VaultFilter } from "../models/vault-filter.model"; @Directive() export class TypeFilterComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() hide = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() collapsedFilterNodes: Set; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() selectedCipherType: CipherType = null; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() activeFilter: VaultFilter; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onNodeCollapseStateChange: EventEmitter = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onFilterChange: EventEmitter = new EventEmitter(); readonly typesNode: TopLevelTreeNode = { diff --git a/libs/angular/src/vault/vault-filter/components/vault-filter.component.ts b/libs/angular/src/vault/vault-filter/components/vault-filter.component.ts index 9199c53bfcb..9b1d6286a9a 100644 --- a/libs/angular/src/vault/vault-filter/components/vault-filter.component.ts +++ b/libs/angular/src/vault/vault-filter/components/vault-filter.component.ts @@ -22,15 +22,33 @@ import { VaultFilter } from "../models/vault-filter.model"; // and refactor desktop/browser vault filters @Directive() export class VaultFilterComponent implements OnInit { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() activeFilter: VaultFilter = new VaultFilter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() hideFolders = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() hideCollections = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() hideFavorites = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() hideTrash = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() hideOrganizations = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onFilterChange = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onAddFolder = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onEditFolder = new EventEmitter(); private activeUserId: UserId; diff --git a/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.spec.ts b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.spec.ts index a9a327b90c0..6a574053367 100644 --- a/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.spec.ts +++ b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.spec.ts @@ -13,11 +13,15 @@ import { CustomFieldsComponent } from "../custom-fields/custom-fields.component" import { AdditionalOptionsSectionComponent } from "./additional-options-section.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-custom-fields", template: "", }) class MockCustomFieldsComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() disableSectionMargin: boolean; } diff --git a/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.ts b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.ts index 3a7152bfe24..f37d4f71f63 100644 --- a/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.ts +++ b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.ts @@ -21,6 +21,8 @@ import { PasswordRepromptService } from "../../../services/password-reprompt.ser import { CipherFormContainer } from "../../cipher-form-container"; import { CustomFieldsComponent } from "../custom-fields/custom-fields.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-additional-options-section", templateUrl: "./additional-options-section.component.html", @@ -39,6 +41,8 @@ import { CustomFieldsComponent } from "../custom-fields/custom-fields.component" ], }) export class AdditionalOptionsSectionComponent implements OnInit { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild(CustomFieldsComponent) customFieldsComponent: CustomFieldsComponent; additionalOptionsForm = this.formBuilder.group({ @@ -56,6 +60,8 @@ export class AdditionalOptionsSectionComponent implements OnInit { /** True when the form is in `partial-edit` mode */ isPartialEdit = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() disableSectionMargin: boolean; /** True when the form allows new fields to be added */ diff --git a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.spec.ts b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.spec.ts index c88ce9f0301..06f62976548 100644 --- a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.spec.ts +++ b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.spec.ts @@ -26,13 +26,21 @@ import { FakeAccountService, mockAccountServiceWith } from "../../../../../commo import { CipherAttachmentsComponent } from "./cipher-attachments.component"; import { DeleteAttachmentComponent } from "./delete-attachment/delete-attachment.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-download-attachment", template: "", }) class MockDownloadAttachmentComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() attachment: AttachmentView; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() cipher: CipherView; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() admin: boolean = false; } diff --git a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts index 9ae1c62bd3e..56c3414a12e 100644 --- a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts +++ b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts @@ -56,6 +56,8 @@ type CipherAttachmentForm = FormGroup<{ file: FormControl; }>; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-cipher-attachments", templateUrl: "./cipher-attachments.component.html", @@ -77,27 +79,43 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit { static attachmentFormID = "attachmentForm"; /** Reference to the file HTMLInputElement */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("fileInput", { read: ElementRef }) private fileInput: ElementRef; /** Reference to the BitSubmitDirective */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild(BitSubmitDirective) bitSubmit: BitSubmitDirective; /** The `id` of the cipher in context */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) cipherId: CipherId; /** The organization ID if this cipher belongs to an organization */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() organizationId?: OrganizationId; /** Denotes if the action is occurring from within the admin console */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() admin: boolean = false; /** An optional submit button, whose loading/disabled state will be tied to the form state. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() submitBtn?: ButtonComponent; /** Emits after a file has been successfully uploaded */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onUploadSuccess = new EventEmitter(); /** Emits after a file has been successfully removed */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onRemoveSuccess = new EventEmitter(); organization: Organization; diff --git a/libs/vault/src/cipher-form/components/attachments/delete-attachment/delete-attachment.component.ts b/libs/vault/src/cipher-form/components/attachments/delete-attachment/delete-attachment.component.ts index 60002ca5924..1bb3e071a0c 100644 --- a/libs/vault/src/cipher-form/components/attachments/delete-attachment/delete-attachment.component.ts +++ b/libs/vault/src/cipher-form/components/attachments/delete-attachment/delete-attachment.component.ts @@ -17,6 +17,8 @@ import { ToastService, } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-delete-attachment", templateUrl: "./delete-attachment.component.html", @@ -24,15 +26,23 @@ import { }) export class DeleteAttachmentComponent { /** Id of the cipher associated with the attachment */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) cipherId!: string; /** The attachment that is can be deleted */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) attachment!: AttachmentView; /** Whether the attachment is being accessed from the admin console */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() admin: boolean = false; /** Emits when the attachment is successfully deleted */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onDeletionSuccess = new EventEmitter(); constructor( diff --git a/libs/vault/src/cipher-form/components/autofill-options/advanced-uri-option-dialog.component.ts b/libs/vault/src/cipher-form/components/autofill-options/advanced-uri-option-dialog.component.ts index e63aa224149..f78c2c170f8 100644 --- a/libs/vault/src/cipher-form/components/autofill-options/advanced-uri-option-dialog.component.ts +++ b/libs/vault/src/cipher-form/components/autofill-options/advanced-uri-option-dialog.component.ts @@ -17,6 +17,8 @@ export type AdvancedUriOptionDialogParams = { onContinue: () => void; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "advanced-uri-option-dialog.component.html", imports: [ButtonLinkDirective, ButtonModule, DialogModule, JslibModule], diff --git a/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts b/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts index 6a2b3e431ca..e6b8b5c9aca 100644 --- a/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts +++ b/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts @@ -36,6 +36,8 @@ interface UriField { matchDetection: UriMatchStrategySetting; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-autofill-options", templateUrl: "./autofill-options.component.html", @@ -60,6 +62,8 @@ export class AutofillOptionsComponent implements OnInit { /** * List of rendered UriOptionComponents. Used for focusing newly added Uri inputs. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChildren(UriOptionComponent) protected uriOptions: QueryList; diff --git a/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.ts b/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.ts index 8b6b6a6490b..b61109a45bb 100644 --- a/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.ts +++ b/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.ts @@ -36,6 +36,8 @@ import { import { AdvancedUriOptionDialogComponent } from "./advanced-uri-option-dialog.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-autofill-uri-option", templateUrl: "./uri-option.component.html", @@ -58,9 +60,13 @@ import { AdvancedUriOptionDialogComponent } from "./advanced-uri-option-dialog.c ], }) export class UriOptionComponent implements ControlValueAccessor { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("uriInput") private inputElement: ElementRef; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("matchDetectionSelect") private matchDetectionSelect: SelectComponent; @@ -92,18 +98,24 @@ export class UriOptionComponent implements ControlValueAccessor { /** * Whether the option can be reordered. If false, the reorder button will be hidden. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) canReorder: boolean; /** * Whether the URI can be removed from the form. If false, the remove button will be hidden. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) canRemove: boolean; /** * The user's current default match detection strategy. Will be displayed in () after "Default" */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) set defaultMatchDetection(value: UriMatchStrategySetting) { // The default selection has a value of `null` avoid showing "Default (Default)" @@ -120,14 +132,20 @@ export class UriOptionComponent implements ControlValueAccessor { /** * The index of the URI in the form. Used to render the correct label. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) index: number; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onKeydown = new EventEmitter(); /** * Emits when the remove button is clicked and URI should be removed from the form. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() remove = new EventEmitter(); diff --git a/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.ts b/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.ts index 7b8149b6d7b..5fa8d0af131 100644 --- a/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.ts +++ b/libs/vault/src/cipher-form/components/card-details-section/card-details-section.component.ts @@ -23,6 +23,8 @@ import { import { CipherFormContainer } from "../../cipher-form-container"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-card-details-section", templateUrl: "./card-details-section.component.html", @@ -40,9 +42,13 @@ import { CipherFormContainer } from "../../cipher-form-container"; }) export class CardDetailsSectionComponent implements OnInit { /** The original cipher */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() originalCipherView: CipherView; /** True when all fields should be disabled */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() disabled: boolean; /** diff --git a/libs/vault/src/cipher-form/components/cipher-form.component.ts b/libs/vault/src/cipher-form/components/cipher-form.component.ts index f7676818edf..5e75ea5bc24 100644 --- a/libs/vault/src/cipher-form/components/cipher-form.component.ts +++ b/libs/vault/src/cipher-form/components/cipher-form.component.ts @@ -49,6 +49,8 @@ import { LoginDetailsSectionComponent } from "./login-details-section/login-deta import { NewItemNudgeComponent } from "./new-item-nudge/new-item-nudge.component"; import { SshKeySectionComponent } from "./sshkey-section/sshkey-section.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-cipher-form", templateUrl: "./cipher-form.component.html", @@ -79,6 +81,8 @@ import { SshKeySectionComponent } from "./sshkey-section/sshkey-section.componen ], }) export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, CipherFormContainer { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild(BitSubmitDirective) private bitSubmit: BitSubmitDirective; private destroyRef = inject(DestroyRef); @@ -87,38 +91,52 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci /** * The form ID to use for the form. Used to connect it to a submit button. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) formId: string; /** * The configuration for the add/edit form. Used to determine which controls are shown and what values are available. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) config: CipherFormConfig; /** * Optional submit button that will be disabled or marked as loading when the form is submitting. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() submitBtn?: ButtonComponent; /** * Optional function to call before submitting the form. If the function returns false, the form will not be submitted. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() beforeSubmit: () => Promise; /** * Event emitted when the cipher is saved successfully. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() cipherSaved = new EventEmitter(); private formReadySubject = new Subject(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() formReady = this.formReadySubject.asObservable(); /** * Emitted when the form is enabled */ private formStatusChangeSubject = new BehaviorSubject<"enabled" | "disabled" | null>(null); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() formStatusChange$ = this.formStatusChangeSubject.asObservable(); /** diff --git a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.spec.ts b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.spec.ts index e98e4805d19..bc2b86f01ff 100644 --- a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.spec.ts +++ b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.spec.ts @@ -6,19 +6,27 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { GeneratorModule } from "@bitwarden/generator-components"; import { CipherFormGeneratorComponent } from "@bitwarden/vault"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "tools-password-generator", template: ``, }) class MockPasswordGeneratorComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onGenerated = new EventEmitter(); } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "tools-username-generator", template: ``, }) class MockUsernameGeneratorComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onGenerated = new EventEmitter(); } diff --git a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts index f1e4c5c177c..e053dd96973 100644 --- a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts +++ b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts @@ -9,30 +9,42 @@ import { AlgorithmInfo, GeneratedCredential } from "@bitwarden/generator-core"; * Renders a password or username generator UI and emits the most recently generated value. * Used by the cipher form to be shown in a dialog/modal when generating cipher passwords/usernames. */ +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-cipher-form-generator", templateUrl: "./cipher-form-generator.component.html", imports: [CommonModule, GeneratorModule], }) export class CipherFormGeneratorComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() uri: string = ""; /** * The type of generator form to show. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) type: "password" | "username" = "password"; /** Removes bottom margin of internal sections */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ transform: coerceBooleanProperty }) disableMargin = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() algorithmSelected = new EventEmitter(); /** * Emits an event when a new value is generated. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() valueGenerated = new EventEmitter(); diff --git a/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.ts b/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.ts index 7d56db4366b..81720f8e612 100644 --- a/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.ts +++ b/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.ts @@ -28,6 +28,8 @@ export type AddEditCustomFieldDialogData = { disallowHiddenField?: boolean; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-add-edit-custom-field-dialog", templateUrl: "./add-edit-custom-field-dialog.component.html", diff --git a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts index 013ccd6c87e..b07d17af7d0 100644 --- a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts +++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts @@ -68,6 +68,8 @@ export type CustomField = { newField: boolean; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-custom-fields", templateUrl: "./custom-fields.component.html", @@ -88,10 +90,16 @@ export type CustomField = { ], }) export class CustomFieldsComponent implements OnInit, AfterViewInit { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() numberOfFieldsChange = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChildren("customFieldRow") customFieldRows: QueryList>; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() disableSectionMargin: boolean; customFieldsForm = this.formBuilder.group({ diff --git a/libs/vault/src/cipher-form/components/identity/identity.component.ts b/libs/vault/src/cipher-form/components/identity/identity.component.ts index 4c90024e05a..642a0cc4aff 100644 --- a/libs/vault/src/cipher-form/components/identity/identity.component.ts +++ b/libs/vault/src/cipher-form/components/identity/identity.component.ts @@ -21,6 +21,8 @@ import { import { CipherFormContainer } from "../../cipher-form-container"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-identity-section", templateUrl: "./identity.component.html", @@ -38,7 +40,11 @@ import { CipherFormContainer } from "../../cipher-form-container"; ], }) export class IdentitySectionComponent implements OnInit { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() originalCipherView: CipherView; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() disabled: boolean; identityTitleOptions = [ { name: "-- " + this.i18nService.t("select") + " --", value: null }, diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts index 892fc5804ec..6fd74d86525 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts @@ -37,6 +37,8 @@ import { } from "../../abstractions/cipher-form-config.service"; import { CipherFormContainer } from "../../cipher-form-container"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-item-details-section", templateUrl: "./item-details-section.component.html", @@ -84,9 +86,13 @@ export class ItemDetailsSectionComponent implements OnInit { protected favoriteButtonDisabled = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) config: CipherFormConfig; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() originalCipherView: CipherView; diff --git a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts index d6fe8a64921..8e60b9f32e0 100644 --- a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts +++ b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts @@ -23,6 +23,8 @@ import { AutofillOptionsComponent } from "../autofill-options/autofill-options.c import { LoginDetailsSectionComponent } from "./login-details-section.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-autofill-options", template: "", diff --git a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts index 061a8c4abf4..8b9c4ddeea1 100644 --- a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts +++ b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts @@ -30,6 +30,8 @@ import { TotpCaptureService } from "../../abstractions/totp-capture.service"; import { CipherFormContainer } from "../../cipher-form-container"; import { AutofillOptionsComponent } from "../autofill-options/autofill-options.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-login-details-section", templateUrl: "./login-details-section.component.html", diff --git a/libs/vault/src/cipher-form/components/new-item-nudge/new-item-nudge.component.ts b/libs/vault/src/cipher-form/components/new-item-nudge/new-item-nudge.component.ts index 70b94505731..5f4a44e5ef5 100644 --- a/libs/vault/src/cipher-form/components/new-item-nudge/new-item-nudge.component.ts +++ b/libs/vault/src/cipher-form/components/new-item-nudge/new-item-nudge.component.ts @@ -11,13 +11,15 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { UserId } from "@bitwarden/common/types/guid"; import { CipherType } from "@bitwarden/sdk-internal"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-new-item-nudge", templateUrl: "./new-item-nudge.component.html", imports: [SpotlightComponent, AsyncPipe], }) export class NewItemNudgeComponent { - configType = input.required(); + readonly configType = input.required(); activeUserId$ = this.accountService.activeAccount$.pipe(getUserId); showNewItemSpotlight$ = combineLatest([ this.activeUserId$, diff --git a/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.ts b/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.ts index f92c4420d03..649dd807f29 100644 --- a/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.ts +++ b/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.ts @@ -25,6 +25,8 @@ import { generate_ssh_key } from "@bitwarden/sdk-internal"; import { SshImportPromptService } from "../../../services/ssh-import-prompt.service"; import { CipherFormContainer } from "../../cipher-form-container"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-sshkey-section", templateUrl: "./sshkey-section.component.html", @@ -42,9 +44,13 @@ import { CipherFormContainer } from "../../cipher-form-container"; }) export class SshKeySectionComponent implements OnInit { /** The original cipher */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() originalCipherView: CipherView; /** True when all fields should be disabled */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() disabled: boolean; /** diff --git a/libs/vault/src/cipher-view/additional-options/additional-options.component.ts b/libs/vault/src/cipher-view/additional-options/additional-options.component.ts index 3e632983d49..4933c137e51 100644 --- a/libs/vault/src/cipher-view/additional-options/additional-options.component.ts +++ b/libs/vault/src/cipher-view/additional-options/additional-options.component.ts @@ -11,6 +11,8 @@ import { FormFieldModule, } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-additional-options", templateUrl: "additional-options.component.html", @@ -26,5 +28,7 @@ import { ], }) export class AdditionalOptionsComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() notes: string = ""; } diff --git a/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.ts b/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.ts index 711c63878e3..4e324d8002e 100644 --- a/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.ts +++ b/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.ts @@ -22,6 +22,8 @@ import { KeyService } from "@bitwarden/key-management"; import { DownloadAttachmentComponent } from "../../components/download-attachment/download-attachment.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-attachments-v2-view", templateUrl: "attachments-v2-view.component.html", @@ -36,11 +38,17 @@ import { DownloadAttachmentComponent } from "../../components/download-attachmen ], }) export class AttachmentsV2ViewComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() cipher: CipherView; // Required for fetching attachment data when viewed from cipher via emergency access + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() emergencyAccessId?: EmergencyAccessId; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() admin: boolean = false; canAccessPremium: boolean; diff --git a/libs/vault/src/cipher-view/attachments/attachments-v2.component.ts b/libs/vault/src/cipher-view/attachments/attachments-v2.component.ts index 11c15f63505..2796cae08d0 100644 --- a/libs/vault/src/cipher-view/attachments/attachments-v2.component.ts +++ b/libs/vault/src/cipher-view/attachments/attachments-v2.component.ts @@ -40,6 +40,8 @@ export interface AttachmentDialogCloseResult { /** * Component for the attachments dialog. */ +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-vault-attachments-v2", templateUrl: "attachments-v2.component.html", diff --git a/libs/vault/src/cipher-view/autofill-options/autofill-options-view.component.ts b/libs/vault/src/cipher-view/autofill-options/autofill-options-view.component.ts index 0643737d846..8bc55fb3760 100644 --- a/libs/vault/src/cipher-view/autofill-options/autofill-options-view.component.ts +++ b/libs/vault/src/cipher-view/autofill-options/autofill-options-view.component.ts @@ -18,6 +18,8 @@ import { TypographyModule, } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-autofill-options-view", templateUrl: "autofill-options-view.component.html", @@ -32,7 +34,11 @@ import { ], }) export class AutofillOptionsViewComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() loginUris: LoginUriView[]; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() cipherId: string; constructor( diff --git a/libs/vault/src/cipher-view/card-details/card-details-view.component.ts b/libs/vault/src/cipher-view/card-details/card-details-view.component.ts index 502214848f3..d80aafde46b 100644 --- a/libs/vault/src/cipher-view/card-details/card-details-view.component.ts +++ b/libs/vault/src/cipher-view/card-details/card-details-view.component.ts @@ -17,6 +17,8 @@ import { import { ReadOnlyCipherCardComponent } from "../read-only-cipher-card/read-only-cipher-card.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-card-details-view", templateUrl: "card-details-view.component.html", @@ -31,6 +33,8 @@ import { ReadOnlyCipherCardComponent } from "../read-only-cipher-card/read-only- ], }) export class CardDetailsComponent implements OnChanges { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() cipher: CipherView; EventType = EventType; diff --git a/libs/vault/src/cipher-view/cipher-view.component.ts b/libs/vault/src/cipher-view/cipher-view.component.ts index 1a294be46aa..15cb7d4651f 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.ts +++ b/libs/vault/src/cipher-view/cipher-view.component.ts @@ -39,6 +39,8 @@ import { LoginCredentialsViewComponent } from "./login-credentials/login-credent import { SshKeyViewComponent } from "./sshkey-sections/sshkey-view.component"; import { ViewIdentitySectionsComponent } from "./view-identity-sections/view-identity-sections.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-cipher-view", templateUrl: "cipher-view.component.html", @@ -61,9 +63,13 @@ import { ViewIdentitySectionsComponent } from "./view-identity-sections/view-ide ], }) export class CipherViewComponent implements OnChanges, OnDestroy { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) cipher: CipherView | null = null; // Required for fetching attachment data when viewed from cipher via emergency access + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() emergencyAccessId?: EmergencyAccessId; activeUserId$ = getUserId(this.accountService.activeAccount$); @@ -72,9 +78,13 @@ export class CipherViewComponent implements OnChanges, OnDestroy { * Optional list of collections the cipher is assigned to. If none are provided, they will be fetched using the * `CipherService` and the `collectionIds` property of the cipher. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() collections?: CollectionView[]; /** Should be set to true when the component is used within the Admin Console */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() isAdminConsole?: boolean = false; organization$: Observable | undefined; diff --git a/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.ts b/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.ts index 7c2afd5029f..8b1eaab74bb 100644 --- a/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.ts +++ b/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.ts @@ -24,6 +24,8 @@ import { import { VaultAutosizeReadOnlyTextArea } from "../../directives/readonly-textarea.directive"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-custom-fields-v2", templateUrl: "custom-fields-v2.component.html", @@ -42,6 +44,8 @@ import { VaultAutosizeReadOnlyTextArea } from "../../directives/readonly-textare ], }) export class CustomFieldV2Component implements OnInit, OnChanges { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) cipher!: CipherView; fieldType = FieldType; fieldOptions: Map | undefined; diff --git a/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts b/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts index 31ba5c82d9d..2c310daad76 100644 --- a/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts +++ b/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts @@ -22,6 +22,8 @@ import { import { OrgIconDirective } from "../../components/org-icon.directive"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-item-details-v2", templateUrl: "item-details-v2.component.html", diff --git a/libs/vault/src/cipher-view/item-history/item-history-v2.component.ts b/libs/vault/src/cipher-view/item-history/item-history-v2.component.ts index 2bbb6418934..1295836d3d9 100644 --- a/libs/vault/src/cipher-view/item-history/item-history-v2.component.ts +++ b/libs/vault/src/cipher-view/item-history/item-history-v2.component.ts @@ -16,6 +16,8 @@ import { TypographyModule, } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-item-history-v2", templateUrl: "item-history-v2.component.html", @@ -31,6 +33,8 @@ import { ], }) export class ItemHistoryV2Component { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() cipher: CipherView; constructor(private viewPasswordHistoryService: ViewPasswordHistoryService) {} diff --git a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts index 5987d055e6b..4dbbf979b15 100644 --- a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts +++ b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts @@ -42,6 +42,8 @@ type TotpCodeValues = { totpCodeFormatted?: string; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-login-credentials-view", templateUrl: "login-credentials-view.component.html", @@ -61,10 +63,20 @@ type TotpCodeValues = { ], }) export class LoginCredentialsViewComponent implements OnChanges { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() cipher: CipherView; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() activeUserId: UserId; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() hadPendingChangePasswordTask: boolean; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() handleChangePassword = new EventEmitter(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("passwordInput") private passwordInput!: ElementRef; diff --git a/libs/vault/src/cipher-view/read-only-cipher-card/read-only-cipher-card.component.ts b/libs/vault/src/cipher-view/read-only-cipher-card/read-only-cipher-card.component.ts index 8f6b9954a9f..7a17376472d 100644 --- a/libs/vault/src/cipher-view/read-only-cipher-card/read-only-cipher-card.component.ts +++ b/libs/vault/src/cipher-view/read-only-cipher-card/read-only-cipher-card.component.ts @@ -2,6 +2,8 @@ import { AfterViewInit, Component, ContentChildren, QueryList } from "@angular/c import { CardComponent, BitFormFieldComponent } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "read-only-cipher-card", templateUrl: "./read-only-cipher-card.component.html", @@ -11,6 +13,8 @@ import { CardComponent, BitFormFieldComponent } from "@bitwarden/components"; * A thin wrapper around the `bit-card` component that disables the bottom border for the last form field. */ export class ReadOnlyCipherCardComponent implements AfterViewInit { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ContentChildren(BitFormFieldComponent) formFields?: QueryList; ngAfterViewInit(): void { diff --git a/libs/vault/src/cipher-view/sshkey-sections/sshkey-view.component.ts b/libs/vault/src/cipher-view/sshkey-sections/sshkey-view.component.ts index 535c41b9aea..5d076d81cc7 100644 --- a/libs/vault/src/cipher-view/sshkey-sections/sshkey-view.component.ts +++ b/libs/vault/src/cipher-view/sshkey-sections/sshkey-view.component.ts @@ -14,6 +14,8 @@ import { import { ReadOnlyCipherCardComponent } from "../read-only-cipher-card/read-only-cipher-card.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-sshkey-view", templateUrl: "sshkey-view.component.html", @@ -28,6 +30,8 @@ import { ReadOnlyCipherCardComponent } from "../read-only-cipher-card/read-only- ], }) export class SshKeyViewComponent implements OnChanges { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() sshKey: SshKeyView; revealSshKey = false; diff --git a/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.ts b/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.ts index f9cb9d2b549..14fb7e2925c 100644 --- a/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.ts +++ b/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.ts @@ -12,6 +12,8 @@ import { import { ReadOnlyCipherCardComponent } from "../read-only-cipher-card/read-only-cipher-card.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-view-identity-sections", templateUrl: "./view-identity-sections.component.html", @@ -26,6 +28,8 @@ import { ReadOnlyCipherCardComponent } from "../read-only-cipher-card/read-only- ], }) export class ViewIdentitySectionsComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) cipher: CipherView | null = null; /** Returns all populated address fields */ diff --git a/libs/vault/src/components/add-edit-folder-dialog/add-edit-folder-dialog.component.ts b/libs/vault/src/components/add-edit-folder-dialog/add-edit-folder-dialog.component.ts index 0442bcd1f76..adc4c67b2f4 100644 --- a/libs/vault/src/components/add-edit-folder-dialog/add-edit-folder-dialog.component.ts +++ b/libs/vault/src/components/add-edit-folder-dialog/add-edit-folder-dialog.component.ts @@ -47,6 +47,8 @@ export type AddEditFolderDialogData = { editFolderConfig?: { folder: FolderView }; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-add-edit-folder-dialog", templateUrl: "./add-edit-folder-dialog.component.html", @@ -62,7 +64,11 @@ export type AddEditFolderDialogData = { ], }) export class AddEditFolderDialogComponent implements AfterViewInit, OnInit { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild(BitSubmitDirective) private bitSubmit?: BitSubmitDirective; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("submitBtn") private submitBtn?: ButtonComponent; folder: FolderView = new FolderView(); diff --git a/libs/vault/src/components/assign-collections.component.ts b/libs/vault/src/components/assign-collections.component.ts index 9890074a8c9..f0ce59b0c3c 100644 --- a/libs/vault/src/components/assign-collections.component.ts +++ b/libs/vault/src/components/assign-collections.component.ts @@ -96,6 +96,8 @@ export type CollectionAssignmentResult = UnionOfValues(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onCollectionAssign = new EventEmitter(); formGroup = this.formBuilder.group({ diff --git a/libs/vault/src/components/can-delete-cipher.directive.ts b/libs/vault/src/components/can-delete-cipher.directive.ts index 7eadedc7ada..8ab59f9d647 100644 --- a/libs/vault/src/components/can-delete-cipher.directive.ts +++ b/libs/vault/src/components/can-delete-cipher.directive.ts @@ -13,6 +13,8 @@ import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cip export class CanDeleteCipherDirective implements OnDestroy { private destroy$ = new Subject(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input("appCanDeleteCipher") set cipher(cipher: CipherView) { this.viewContainer.clear(); diff --git a/libs/vault/src/components/carousel/carousel-button/carousel-button.component.ts b/libs/vault/src/components/carousel/carousel-button/carousel-button.component.ts index ae2ce12cba8..bef7f5b12d6 100644 --- a/libs/vault/src/components/carousel/carousel-button/carousel-button.component.ts +++ b/libs/vault/src/components/carousel/carousel-button/carousel-button.component.ts @@ -7,6 +7,8 @@ import { IconModule } from "@bitwarden/components"; import { VaultCarouselSlideComponent } from "../carousel-slide/carousel-slide.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-carousel-button", templateUrl: "carousel-button.component.html", @@ -14,15 +16,23 @@ import { VaultCarouselSlideComponent } from "../carousel-slide/carousel-slide.co }) export class VaultCarouselButtonComponent implements FocusableOption { /** Slide component that is associated with the individual button */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) slide!: VaultCarouselSlideComponent; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("btn", { static: true }) button!: ElementRef; protected CarouselIcon = CarouselIcon; /** When set to true the button is shown in an active state. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) isActive!: boolean; /** Emits when the button is clicked. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onClick = new EventEmitter(); /** Focuses the underlying button element. */ diff --git a/libs/vault/src/components/carousel/carousel-content/carousel-content.component.spec.ts b/libs/vault/src/components/carousel/carousel-content/carousel-content.component.spec.ts index bc1c9250c2c..5d396984f17 100644 --- a/libs/vault/src/components/carousel/carousel-content/carousel-content.component.spec.ts +++ b/libs/vault/src/components/carousel/carousel-content/carousel-content.component.spec.ts @@ -5,6 +5,8 @@ import { By } from "@angular/platform-browser"; import { VaultCarouselContentComponent } from "./carousel-content.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-test-template-ref", imports: [VaultCarouselContentComponent], @@ -17,6 +19,8 @@ import { VaultCarouselContentComponent } from "./carousel-content.component"; }) class TestTemplateRefComponent implements OnInit { // Test template content by creating a wrapping component and then pass a portal to the carousel content component. + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("template", { static: true }) template!: TemplateRef; portal!: TemplatePortal; diff --git a/libs/vault/src/components/carousel/carousel-content/carousel-content.component.ts b/libs/vault/src/components/carousel/carousel-content/carousel-content.component.ts index 47027a77ae9..a3c3a9f1caf 100644 --- a/libs/vault/src/components/carousel/carousel-content/carousel-content.component.ts +++ b/libs/vault/src/components/carousel/carousel-content/carousel-content.component.ts @@ -1,6 +1,8 @@ import { TemplatePortal, CdkPortalOutlet } from "@angular/cdk/portal"; import { Component, Input } from "@angular/core"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-carousel-content", templateUrl: "carousel-content.component.html", @@ -8,5 +10,7 @@ import { Component, Input } from "@angular/core"; }) export class VaultCarouselContentComponent { /** Content to be displayed for the carousel. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) content!: TemplatePortal; } diff --git a/libs/vault/src/components/carousel/carousel-slide/carousel-slide.component.spec.ts b/libs/vault/src/components/carousel/carousel-slide/carousel-slide.component.spec.ts index 46f06f6dcb4..116403362f4 100644 --- a/libs/vault/src/components/carousel/carousel-slide/carousel-slide.component.spec.ts +++ b/libs/vault/src/components/carousel/carousel-slide/carousel-slide.component.spec.ts @@ -5,6 +5,8 @@ import { By } from "@angular/platform-browser"; import { VaultCarouselSlideComponent } from "./carousel-slide.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-test-carousel-slide", imports: [VaultCarouselSlideComponent], diff --git a/libs/vault/src/components/carousel/carousel-slide/carousel-slide.component.ts b/libs/vault/src/components/carousel/carousel-slide/carousel-slide.component.ts index 811572881da..973a615d6f9 100644 --- a/libs/vault/src/components/carousel/carousel-slide/carousel-slide.component.ts +++ b/libs/vault/src/components/carousel/carousel-slide/carousel-slide.component.ts @@ -11,6 +11,8 @@ import { ViewContainerRef, } from "@angular/core"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-carousel-slide", templateUrl: "./carousel-slide.component.html", @@ -18,7 +20,11 @@ import { }) export class VaultCarouselSlideComponent implements OnInit { /** `aria-label` that is assigned to the carousel toggle. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) label!: string; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ transform: booleanAttribute }) disablePadding = false; /** @@ -29,8 +35,12 @@ export class VaultCarouselSlideComponent implements OnInit { * * @remarks See note 4 of https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/ */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ transform: coerceBooleanProperty }) noFocusableChildren?: true; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild(TemplateRef, { static: true }) implicitContent!: TemplateRef; private _contentPortal: TemplatePortal | null = null; diff --git a/libs/vault/src/components/carousel/carousel.component.spec.ts b/libs/vault/src/components/carousel/carousel.component.spec.ts index ebb38576813..abbfe963ddf 100644 --- a/libs/vault/src/components/carousel/carousel.component.spec.ts +++ b/libs/vault/src/components/carousel/carousel.component.spec.ts @@ -7,6 +7,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { VaultCarouselSlideComponent } from "./carousel-slide/carousel-slide.component"; import { VaultCarouselComponent } from "./carousel.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-test-carousel-slide", imports: [VaultCarouselComponent, VaultCarouselSlideComponent], diff --git a/libs/vault/src/components/carousel/carousel.component.ts b/libs/vault/src/components/carousel/carousel.component.ts index fdebbebc33b..4e180f09f9b 100644 --- a/libs/vault/src/components/carousel/carousel.component.ts +++ b/libs/vault/src/components/carousel/carousel.component.ts @@ -28,6 +28,8 @@ import { VaultCarouselButtonComponent } from "./carousel-button/carousel-button. import { VaultCarouselContentComponent } from "./carousel-content/carousel-content.component"; import { VaultCarouselSlideComponent } from "./carousel-slide/carousel-slide.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-carousel", templateUrl: "./carousel.component.html", @@ -50,30 +52,46 @@ export class VaultCarouselComponent implements AfterViewInit { * @remarks * The label should not include the word "carousel", `aria-roledescription="carousel"` is already included. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) label = ""; /** * Emits the index of the newly selected slide. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() slideChange = new EventEmitter(); /** All slides within the carousel. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ContentChildren(VaultCarouselSlideComponent) slides!: QueryList; /** All buttons that control the carousel */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChildren(VaultCarouselButtonComponent) carouselButtons!: QueryList; /** Wrapping container for the carousel content and buttons */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("container") carouselContainer!: ElementRef; /** Container for the carousel buttons */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("carouselButtonWrapper") carouselButtonWrapper!: ElementRef; /** Temporary container containing `tempSlideOutlet` */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("tempSlideContainer") tempSlideContainer!: ElementRef; /** Outlet to temporary render each slide within */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild(CdkPortalOutlet) tempSlideOutlet!: CdkPortalOutlet; /** The currently selected index of the carousel. */ diff --git a/libs/vault/src/components/copy-cipher-field.directive.ts b/libs/vault/src/components/copy-cipher-field.directive.ts index 9725adae5e2..7e8ca334f9e 100644 --- a/libs/vault/src/components/copy-cipher-field.directive.ts +++ b/libs/vault/src/components/copy-cipher-field.directive.ts @@ -30,13 +30,18 @@ import { CopyAction, CopyCipherFieldService } from "@bitwarden/vault"; selector: "[appCopyField]", }) export class CopyCipherFieldDirective implements OnChanges { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ alias: "appCopyField", required: true, }) action!: Exclude; - @Input({ required: true }) cipher!: CipherViewLike; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals + @Input({ required: true }) + cipher!: CipherViewLike; constructor( private copyCipherFieldService: CopyCipherFieldService, diff --git a/libs/vault/src/components/dark-image-source.directive.ts b/libs/vault/src/components/dark-image-source.directive.ts index ee54f61209a..b899ad472d4 100644 --- a/libs/vault/src/components/dark-image-source.directive.ts +++ b/libs/vault/src/components/dark-image-source.directive.ts @@ -40,7 +40,7 @@ export class DarkImageSourceDirective implements OnInit { /** * The image source to use when the dark theme is applied. */ - darkImgSrc = input.required({ alias: "appDarkImgSrc" }); + readonly darkImgSrc = input.required({ alias: "appDarkImgSrc" }); @HostBinding("attr.src") src: string | undefined; diff --git a/libs/vault/src/components/decryption-failure-dialog/decryption-failure-dialog.component.ts b/libs/vault/src/components/decryption-failure-dialog/decryption-failure-dialog.component.ts index 91b1cef364c..628de79b3da 100644 --- a/libs/vault/src/components/decryption-failure-dialog/decryption-failure-dialog.component.ts +++ b/libs/vault/src/components/decryption-failure-dialog/decryption-failure-dialog.component.ts @@ -19,6 +19,8 @@ export type DecryptionFailureDialogParams = { cipherIds: CipherId[]; }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-decryption-failure-dialog", templateUrl: "./decryption-failure-dialog.component.html", diff --git a/libs/vault/src/components/download-attachment/download-attachment.component.ts b/libs/vault/src/components/download-attachment/download-attachment.component.ts index 8208887b888..2f9cd528990 100644 --- a/libs/vault/src/components/download-attachment/download-attachment.component.ts +++ b/libs/vault/src/components/download-attachment/download-attachment.component.ts @@ -17,6 +17,8 @@ import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.v import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { AsyncActionsModule, IconButtonModule, ToastService } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-download-attachment", templateUrl: "./download-attachment.component.html", @@ -24,18 +26,28 @@ import { AsyncActionsModule, IconButtonModule, ToastService } from "@bitwarden/c }) export class DownloadAttachmentComponent { /** Attachment to download */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) attachment: AttachmentView; /** The cipher associated with the attachment */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) cipher: CipherView; // When in view mode, we will want to check for the master password reprompt + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() checkPwReprompt?: boolean = false; // Required for fetching attachment data when viewed from cipher via emergency access + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() emergencyAccessId?: EmergencyAccessId; /** When owners/admins can mange all items and when accessing from the admin console, use the admin endpoint */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() admin?: boolean = false; constructor( diff --git a/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.ts b/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.ts index 82bbc9a0749..0a755a9cdb4 100644 --- a/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.ts +++ b/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.ts @@ -9,15 +9,25 @@ import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-ite import { ButtonModule, MenuModule } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-new-cipher-menu", templateUrl: "new-cipher-menu.component.html", imports: [ButtonModule, CommonModule, MenuModule, I18nPipe, JslibModule], }) export class NewCipherMenuComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals canCreateCipher = input(false); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals canCreateFolder = input(false); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals canCreateCollection = input(false); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals canCreateSshKey = input(false); folderAdded = output(); collectionAdded = output(); diff --git a/libs/vault/src/components/org-icon.directive.ts b/libs/vault/src/components/org-icon.directive.ts index d9c8f240474..e9f28cb246a 100644 --- a/libs/vault/src/components/org-icon.directive.ts +++ b/libs/vault/src/components/org-icon.directive.ts @@ -8,7 +8,11 @@ export type OrgIconSize = "default" | "small" | "large"; selector: "[appOrgIcon]", }) export class OrgIconDirective { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) tierType!: ProductTierType; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() size?: OrgIconSize = "default"; constructor( diff --git a/libs/vault/src/components/password-history-view/password-history-view.component.ts b/libs/vault/src/components/password-history-view/password-history-view.component.ts index 427644f3e77..e7d64cfdfdc 100644 --- a/libs/vault/src/components/password-history-view/password-history-view.component.ts +++ b/libs/vault/src/components/password-history-view/password-history-view.component.ts @@ -8,6 +8,8 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { PasswordHistoryView } from "@bitwarden/common/vault/models/view/password-history.view"; import { ItemModule, ColorPasswordModule, IconButtonModule } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-password-history-view", templateUrl: "./password-history-view.component.html", @@ -17,6 +19,8 @@ export class PasswordHistoryViewComponent implements OnInit { /** * Optional cipher view. When included `cipherId` is ignored. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) cipher: CipherView; /** The password history for the cipher. */ diff --git a/libs/vault/src/components/password-history/password-history.component.ts b/libs/vault/src/components/password-history/password-history.component.ts index 7845edb2369..dd2865fa2ce 100644 --- a/libs/vault/src/components/password-history/password-history.component.ts +++ b/libs/vault/src/components/password-history/password-history.component.ts @@ -26,6 +26,8 @@ export interface ViewPasswordHistoryDialogParams { /** * A dialog component that displays the password history for a cipher. */ +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "app-vault-password-history", templateUrl: "password-history.component.html", diff --git a/libs/vault/src/components/password-reprompt.component.ts b/libs/vault/src/components/password-reprompt.component.ts index 7665b22be49..f5245f5cad6 100644 --- a/libs/vault/src/components/password-reprompt.component.ts +++ b/libs/vault/src/components/password-reprompt.component.ts @@ -22,6 +22,8 @@ import { KeyService } from "@bitwarden/key-management"; * Used to verify the user's Master Password for the "Master Password Re-prompt" feature only. * See UserVerificationComponent for any other situation where you need to verify the user's identity. */ +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-password-reprompt", imports: [ diff --git a/libs/vault/src/components/permit-cipher-details-popover/permit-cipher-details-popover.component.ts b/libs/vault/src/components/permit-cipher-details-popover/permit-cipher-details-popover.component.ts index 8e80ddf7810..3649c8a21e1 100644 --- a/libs/vault/src/components/permit-cipher-details-popover/permit-cipher-details-popover.component.ts +++ b/libs/vault/src/components/permit-cipher-details-popover/permit-cipher-details-popover.component.ts @@ -4,6 +4,8 @@ import { JslibModule } from "@bitwarden/angular/jslib.module"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { LinkModule, PopoverModule } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-permit-cipher-details-popover", templateUrl: "./permit-cipher-details-popover.component.html", diff --git a/libs/vault/src/components/totp-countdown/totp-countdown.component.ts b/libs/vault/src/components/totp-countdown/totp-countdown.component.ts index 5274ce621f0..32f9a64bb87 100644 --- a/libs/vault/src/components/totp-countdown/totp-countdown.component.ts +++ b/libs/vault/src/components/totp-countdown/totp-countdown.component.ts @@ -15,13 +15,19 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { TotpInfo } from "@bitwarden/common/vault/services/totp.service"; import { TypographyModule } from "@bitwarden/components"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "[bitTotpCountdown]", templateUrl: "totp-countdown.component.html", imports: [CommonModule, TypographyModule], }) export class BitTotpCountdownComponent implements OnInit, OnChanges { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) cipher!: CipherView; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() sendCopyCode = new EventEmitter(); /** From f452f39f3c1b6204876602e4212788241cea0c3d Mon Sep 17 00:00:00 2001 From: Bryan Cunningham Date: Mon, 27 Oct 2025 11:14:42 -0400 Subject: [PATCH 48/73] [CL-847] Card consolidation (#16952) * created shared card directive * WIP * use base card in anon layout * use bit-card for pricing card component * add base card to integration cards * add base card to reports cards * add base card to integration card * use card content on report card * use base card directive on base component * update dirt card to use bit-card * run prettier. fix whitespace * add missing imports to report list stories * add base card story and docs --- .../report-card/report-card.component.html | 10 ++--- .../shared/report-card/report-card.stories.ts | 17 +++++++- .../shared/report-list/report-list.stories.ts | 17 +++++++- .../reports/shared/reports-shared.module.ts | 4 +- .../integration-card.component.html | 17 ++++---- .../integration-card.component.ts | 10 ++++- .../anon-layout/anon-layout.component.html | 6 +-- .../src/anon-layout/anon-layout.component.ts | 10 ++++- .../src/card/base-card/base-card.component.ts | 14 +++++++ .../src/card/base-card/base-card.directive.ts | 9 ++++ .../src/card/base-card/base-card.mdx | 23 +++++++++++ .../src/card/base-card/base-card.stories.ts | 41 +++++++++++++++++++ libs/components/src/card/base-card/index.ts | 2 + .../src/card/card-content.component.ts | 7 ++++ libs/components/src/card/card.component.ts | 6 ++- libs/components/src/card/card.stories.ts | 15 +------ libs/components/src/card/index.ts | 2 + libs/dirt/card/src/card.component.html | 14 ++++--- libs/dirt/card/src/card.component.ts | 8 +--- .../pricing-card/pricing-card.component.html | 6 +-- .../pricing-card/pricing-card.component.ts | 3 +- 21 files changed, 184 insertions(+), 57 deletions(-) create mode 100644 libs/components/src/card/base-card/base-card.component.ts create mode 100644 libs/components/src/card/base-card/base-card.directive.ts create mode 100644 libs/components/src/card/base-card/base-card.mdx create mode 100644 libs/components/src/card/base-card/base-card.stories.ts create mode 100644 libs/components/src/card/base-card/index.ts create mode 100644 libs/components/src/card/card-content.component.ts diff --git a/apps/web/src/app/dirt/reports/shared/report-card/report-card.component.html b/apps/web/src/app/dirt/reports/shared/report-card/report-card.component.html index 8db0db3b5e6..dab928e6ec3 100644 --- a/apps/web/src/app/dirt/reports/shared/report-card/report-card.component.html +++ b/apps/web/src/app/dirt/reports/shared/report-card/report-card.component.html @@ -1,8 +1,8 @@ -
+
-
+

{{ title }}

{{ description }}

-
+ {{ "premium" | i18n }}
{{ "upgrade" | i18n }} -
+ diff --git a/apps/web/src/app/dirt/reports/shared/report-card/report-card.stories.ts b/apps/web/src/app/dirt/reports/shared/report-card/report-card.stories.ts index 76951bf9451..50798fea6e1 100644 --- a/apps/web/src/app/dirt/reports/shared/report-card/report-card.stories.ts +++ b/apps/web/src/app/dirt/reports/shared/report-card/report-card.stories.ts @@ -4,7 +4,12 @@ import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/an import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { BadgeModule, IconModule } from "@bitwarden/components"; +import { + BadgeModule, + BaseCardComponent, + IconModule, + CardContentComponent, +} from "@bitwarden/components"; import { PreloadedEnglishI18nModule } from "../../../../core/tests"; import { ReportVariant } from "../models/report-variant"; @@ -16,7 +21,15 @@ export default { component: ReportCardComponent, decorators: [ moduleMetadata({ - imports: [JslibModule, BadgeModule, IconModule, RouterTestingModule, PremiumBadgeComponent], + imports: [ + JslibModule, + BadgeModule, + CardContentComponent, + IconModule, + RouterTestingModule, + PremiumBadgeComponent, + BaseCardComponent, + ], }), applicationConfig({ providers: [importProvidersFrom(PreloadedEnglishI18nModule)], diff --git a/apps/web/src/app/dirt/reports/shared/report-list/report-list.stories.ts b/apps/web/src/app/dirt/reports/shared/report-list/report-list.stories.ts index 22c7e851bed..5a89eeff803 100644 --- a/apps/web/src/app/dirt/reports/shared/report-list/report-list.stories.ts +++ b/apps/web/src/app/dirt/reports/shared/report-list/report-list.stories.ts @@ -4,7 +4,12 @@ import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/an import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { BadgeModule, IconModule } from "@bitwarden/components"; +import { + BadgeModule, + BaseCardComponent, + CardContentComponent, + IconModule, +} from "@bitwarden/components"; import { PreloadedEnglishI18nModule } from "../../../../core/tests"; import { reports } from "../../reports"; @@ -18,7 +23,15 @@ export default { component: ReportListComponent, decorators: [ moduleMetadata({ - imports: [JslibModule, BadgeModule, RouterTestingModule, IconModule, PremiumBadgeComponent], + imports: [ + JslibModule, + BadgeModule, + RouterTestingModule, + IconModule, + PremiumBadgeComponent, + CardContentComponent, + BaseCardComponent, + ], declarations: [ReportCardComponent], }), applicationConfig({ diff --git a/apps/web/src/app/dirt/reports/shared/reports-shared.module.ts b/apps/web/src/app/dirt/reports/shared/reports-shared.module.ts index cad5d06d798..59e59a6a500 100644 --- a/apps/web/src/app/dirt/reports/shared/reports-shared.module.ts +++ b/apps/web/src/app/dirt/reports/shared/reports-shared.module.ts @@ -1,13 +1,15 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; +import { BaseCardComponent, CardContentComponent } from "@bitwarden/components"; + import { SharedModule } from "../../../shared/shared.module"; import { ReportCardComponent } from "./report-card/report-card.component"; import { ReportListComponent } from "./report-list/report-list.component"; @NgModule({ - imports: [CommonModule, SharedModule], + imports: [CommonModule, SharedModule, BaseCardComponent, CardContentComponent], declarations: [ReportCardComponent, ReportListComponent], exports: [ReportCardComponent, ReportListComponent], }) diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.html b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.html index 423b0130385..19a12755ca0 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.html @@ -1,5 +1,5 @@ -
@@ -27,8 +27,8 @@ }
-
-

+ +

{{ name }} @if (showConnectedBadge()) { @@ -41,8 +41,9 @@ }

-

{{ description }}

- + @if (description) { +

{{ description }}

+ } @if (canSetupConnection) {

-
+ + diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts index f1b0f982d57..e6d4aff05fb 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts @@ -20,7 +20,13 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { ThemeType } from "@bitwarden/common/platform/enums"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { OrganizationId } from "@bitwarden/common/types/guid"; -import { DialogRef, DialogService, ToastService } from "@bitwarden/components"; +import { + BaseCardComponent, + CardContentComponent, + DialogRef, + DialogService, + ToastService, +} from "@bitwarden/components"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { @@ -37,7 +43,7 @@ import { @Component({ selector: "app-integration-card", templateUrl: "./integration-card.component.html", - imports: [SharedModule], + imports: [SharedModule, BaseCardComponent, CardContentComponent], }) export class IntegrationCardComponent implements AfterViewInit, OnDestroy { private destroyed$: Subject = new Subject(); diff --git a/libs/components/src/anon-layout/anon-layout.component.html b/libs/components/src/anon-layout/anon-layout.component.html index f88bdd3f920..15f7d107542 100644 --- a/libs/components/src/anon-layout/anon-layout.component.html +++ b/libs/components/src/anon-layout/anon-layout.component.html @@ -48,11 +48,11 @@
} @else { -
-
+ } diff --git a/libs/components/src/anon-layout/anon-layout.component.ts b/libs/components/src/anon-layout/anon-layout.component.ts index 596a54f8825..e6572a0c3c1 100644 --- a/libs/components/src/anon-layout/anon-layout.component.ts +++ b/libs/components/src/anon-layout/anon-layout.component.ts @@ -21,6 +21,7 @@ import { ClientType } from "@bitwarden/common/enums"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { BaseCardComponent } from "../card"; import { IconModule } from "../icon"; import { SharedModule } from "../shared"; import { TypographyModule } from "../typography"; @@ -32,7 +33,14 @@ export type AnonLayoutMaxWidth = "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl"; @Component({ selector: "auth-anon-layout", templateUrl: "./anon-layout.component.html", - imports: [IconModule, CommonModule, TypographyModule, SharedModule, RouterModule], + imports: [ + IconModule, + CommonModule, + TypographyModule, + SharedModule, + RouterModule, + BaseCardComponent, + ], }) export class AnonLayoutComponent implements OnInit, OnChanges { @HostBinding("class") diff --git a/libs/components/src/card/base-card/base-card.component.ts b/libs/components/src/card/base-card/base-card.component.ts new file mode 100644 index 00000000000..44f82a32c47 --- /dev/null +++ b/libs/components/src/card/base-card/base-card.component.ts @@ -0,0 +1,14 @@ +import { Component } from "@angular/core"; + +import { BaseCardDirective } from "./base-card.directive"; + +/** + * The base card component is a container that applies our standard card border and box-shadow. + * In most cases using our `` component should suffice. + */ +@Component({ + selector: "bit-base-card", + template: ``, + hostDirectives: [BaseCardDirective], +}) +export class BaseCardComponent {} diff --git a/libs/components/src/card/base-card/base-card.directive.ts b/libs/components/src/card/base-card/base-card.directive.ts new file mode 100644 index 00000000000..7c6ec2b3b2f --- /dev/null +++ b/libs/components/src/card/base-card/base-card.directive.ts @@ -0,0 +1,9 @@ +import { Directive } from "@angular/core"; + +@Directive({ + host: { + class: + "tw-box-border tw-block tw-bg-background tw-text-main tw-border tw-border-solid tw-border-secondary-100 tw-shadow tw-rounded-xl", + }, +}) +export class BaseCardDirective {} diff --git a/libs/components/src/card/base-card/base-card.mdx b/libs/components/src/card/base-card/base-card.mdx new file mode 100644 index 00000000000..df326462906 --- /dev/null +++ b/libs/components/src/card/base-card/base-card.mdx @@ -0,0 +1,23 @@ +import { Meta, Primary, Controls, Canvas, Title, Description } from "@storybook/addon-docs"; + +import * as stories from "./base-card.stories"; + + + +```ts +import { BaseCardComponent } from "@bitwarden/components"; +``` + + +<Description /> + +<Canvas of={stories.Default} /> + +## BaseCardDirective + +There is also a `BaseCardDirective` available for use as a hostDirective if need be. But, most +likely using `<bit-base-card>` in your template will do. + +```ts +import { BaseCardDirective } from "@bitwarden/components"; +``` diff --git a/libs/components/src/card/base-card/base-card.stories.ts b/libs/components/src/card/base-card/base-card.stories.ts new file mode 100644 index 00000000000..bae07dd1468 --- /dev/null +++ b/libs/components/src/card/base-card/base-card.stories.ts @@ -0,0 +1,41 @@ +import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; + +import { AnchorLinkDirective } from "../../link"; +import { TypographyModule } from "../../typography"; + +import { BaseCardComponent } from "./base-card.component"; + +export default { + title: "Component Library/Cards/BaseCard", + component: BaseCardComponent, + decorators: [ + moduleMetadata({ + imports: [AnchorLinkDirective, TypographyModule], + }), + ], + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-28355&t=b5tDKylm5sWm2yKo-4", + }, + }, +} as Meta; + +type Story = StoryObj<BaseCardComponent>; + +/** Cards are presentational containers. */ +export const Default: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + <bit-base-card> + <p bitTypography="body1" class="!tw-mb-0"> + The <code><bit-base-card></code> component is a container that applies our standard border and box-shadow. In most cases, <code><bit-card></code> should be used for consistency + </p> + <p bitTypography="body1" class="!tw-mb-0"> + <code><bit-base-card></code> is used in the <a bitLink href="/?path=/story/web-reports-card--enabled">ReportCardComponent</a> and <strong>IntegrationsCardComponent</strong> since they have custom padding requirements + </p> + </bit-base-card> + `, + }), +}; diff --git a/libs/components/src/card/base-card/index.ts b/libs/components/src/card/base-card/index.ts new file mode 100644 index 00000000000..186f2e68f24 --- /dev/null +++ b/libs/components/src/card/base-card/index.ts @@ -0,0 +1,2 @@ +export * from "./base-card.component"; +export * from "./base-card.directive"; diff --git a/libs/components/src/card/card-content.component.ts b/libs/components/src/card/card-content.component.ts new file mode 100644 index 00000000000..60be20e78f0 --- /dev/null +++ b/libs/components/src/card/card-content.component.ts @@ -0,0 +1,7 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "bit-card-content", + template: `<div class="tw-p-4 [@media(min-width:650px)]:tw-p-6"><ng-content></ng-content></div>`, +}) +export class CardContentComponent {} diff --git a/libs/components/src/card/card.component.ts b/libs/components/src/card/card.component.ts index d7e36d1ea9e..9cca973f003 100644 --- a/libs/components/src/card/card.component.ts +++ b/libs/components/src/card/card.component.ts @@ -1,12 +1,14 @@ import { ChangeDetectionStrategy, Component } from "@angular/core"; +import { BaseCardDirective } from "./base-card/base-card.directive"; + @Component({ selector: "bit-card", template: `<ng-content></ng-content>`, changeDetection: ChangeDetectionStrategy.OnPush, host: { - class: - "tw-box-border tw-block tw-bg-background tw-text-main tw-border-solid tw-border-b tw-border-0 tw-border-b-secondary-300 [&:not(bit-layout_*)]:tw-rounded-lg [&:not(bit-layout_*)]:tw-border-b-shadow tw-py-4 bit-compact:tw-py-3 tw-px-3 bit-compact:tw-px-2", + class: "tw-p-4 [@media(min-width:650px)]:tw-p-6", }, + hostDirectives: [BaseCardDirective], }) export class CardComponent {} diff --git a/libs/components/src/card/card.stories.ts b/libs/components/src/card/card.stories.ts index 411cc8e83cc..77faceb8eb7 100644 --- a/libs/components/src/card/card.stories.ts +++ b/libs/components/src/card/card.stories.ts @@ -11,7 +11,7 @@ import { I18nMockService } from "../utils/i18n-mock.service"; import { CardComponent } from "./card.component"; export default { - title: "Component Library/Card", + title: "Component Library/Cards/Card", component: CardComponent, decorators: [ moduleMetadata({ @@ -84,16 +84,3 @@ export const WithinSections: Story = { `, }), }; - -export const WithoutBorderRadius: Story = { - render: (args) => ({ - props: args, - template: /*html*/ ` - <bit-layout> - <bit-card> - <p bitTypography="body1" class="!tw-mb-0">Cards used in <code class="tw-text-danger-700">bit-layout</code> will not have a border radius</p> - </bit-card> - </bit-layout> - `, - }), -}; diff --git a/libs/components/src/card/index.ts b/libs/components/src/card/index.ts index 8151bac4c8b..1027f9b1fe2 100644 --- a/libs/components/src/card/index.ts +++ b/libs/components/src/card/index.ts @@ -1 +1,3 @@ +export * from "./base-card"; export * from "./card.component"; +export * from "./card-content.component"; diff --git a/libs/dirt/card/src/card.component.html b/libs/dirt/card/src/card.component.html index 3fd9372087c..8688cd8fd2c 100644 --- a/libs/dirt/card/src/card.component.html +++ b/libs/dirt/card/src/card.component.html @@ -1,7 +1,9 @@ -<div class="tw-flex-col"> - <span bitTypography="body2" class="tw-flex tw-text-muted">{{ title }}</span> - <div class="tw-flex tw-items-baseline tw-gap-2"> - <span bitTypography="h1">{{ value }}</span> - <span bitTypography="body2">{{ "cardMetrics" | i18n: maxValue }}</span> +<bit-card> + <div class="tw-flex tw-flex-col tw-gap-1.5"> + <span bitTypography="body2" class="tw-flex tw-text-muted">{{ title }}</span> + <div class="tw-flex tw-items-baseline tw-gap-2"> + <span bitTypography="h1" class="!tw-mb-0">{{ value }}</span> + <span bitTypography="body2">{{ "cardMetrics" | i18n: maxValue }}</span> + </div> </div> -</div> +</bit-card> diff --git a/libs/dirt/card/src/card.component.ts b/libs/dirt/card/src/card.component.ts index b9f2e7aa72e..089115fc2bf 100644 --- a/libs/dirt/card/src/card.component.ts +++ b/libs/dirt/card/src/card.component.ts @@ -4,18 +4,14 @@ import { CommonModule } from "@angular/common"; import { Component, Input } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { TypographyModule } from "@bitwarden/components"; +import { TypographyModule, CardComponent as BitCardComponent } from "@bitwarden/components"; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "dirt-card", templateUrl: "./card.component.html", - imports: [CommonModule, TypographyModule, JslibModule], - host: { - class: - "tw-box-border tw-bg-background tw-block tw-text-main tw-border-solid tw-border tw-border-secondary-300 tw-border [&:not(bit-layout_*)]:tw-rounded-lg tw-rounded-lg tw-p-6", - }, + imports: [CommonModule, TypographyModule, JslibModule, BitCardComponent], }) export class CardComponent { /** diff --git a/libs/pricing/src/components/pricing-card/pricing-card.component.html b/libs/pricing/src/components/pricing-card/pricing-card.component.html index 8eae7088ac9..bc0ca68c5c3 100644 --- a/libs/pricing/src/components/pricing-card/pricing-card.component.html +++ b/libs/pricing/src/components/pricing-card/pricing-card.component.html @@ -1,6 +1,4 @@ -<div - class="tw-box-border tw-bg-background tw-text-main tw-border tw-border-secondary-100 tw-rounded-3xl tw-p-8 tw-shadow-sm tw-size-full tw-flex tw-flex-col" -> +<bit-card class="tw-size-full tw-flex tw-flex-col"> <!-- Title Section with Active Badge --> <div class="tw-flex tw-items-center tw-justify-between tw-mb-2"> <ng-content select="[slot=title]"></ng-content> @@ -82,4 +80,4 @@ } } </div> -</div> +</bit-card> diff --git a/libs/pricing/src/components/pricing-card/pricing-card.component.ts b/libs/pricing/src/components/pricing-card/pricing-card.component.ts index a8fed031adf..f268c654331 100644 --- a/libs/pricing/src/components/pricing-card/pricing-card.component.ts +++ b/libs/pricing/src/components/pricing-card/pricing-card.component.ts @@ -6,6 +6,7 @@ import { BadgeVariant, ButtonModule, ButtonType, + CardComponent, IconModule, TypographyModule, } from "@bitwarden/components"; @@ -20,7 +21,7 @@ import { @Component({ selector: "billing-pricing-card", templateUrl: "./pricing-card.component.html", - imports: [BadgeModule, ButtonModule, IconModule, TypographyModule, CurrencyPipe], + imports: [BadgeModule, ButtonModule, IconModule, TypographyModule, CurrencyPipe, CardComponent], }) export class PricingCardComponent { readonly tagline = input.required<string>(); From 93227324bf69ecb9bd765beb270920b106bc3612 Mon Sep 17 00:00:00 2001 From: tangowithfoxtrot <5676771+tangowithfoxtrot@users.noreply.github.com> Date: Mon, 27 Oct 2025 08:22:13 -0700 Subject: [PATCH 49/73] [SM-1465] - Add Terraform provider to integrations page (#16876) * fix: add Datadog org integration service to SM integrations module * misc: add Terraform provider integration card * misc: update Ansible integration link --- .../images/secrets-manager/integrations/terraform.svg | 6 ++++++ .../integrations/integrations.component.spec.ts | 8 +++++++- .../integrations/integrations.component.ts | 9 ++++++++- 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/images/secrets-manager/integrations/terraform.svg diff --git a/apps/web/src/images/secrets-manager/integrations/terraform.svg b/apps/web/src/images/secrets-manager/integrations/terraform.svg new file mode 100644 index 00000000000..813e95e0200 --- /dev/null +++ b/apps/web/src/images/secrets-manager/integrations/terraform.svg @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="98" height="111" viewBox="0 0 98 111" fill="none"> + <path d="M67.34 37.0927V72.1132L97.68 54.6122V19.5547L67.34 37.0927Z" fill="#4040B2"></path> + <path d="M33.6699 19.5547L64.0099 37.0927V72.1132L33.6699 54.5937V19.5547Z" fill="#5C4EE5"></path> + <path d="M0 0V35.039L30.34 52.5585V17.5195L0 0ZM33.67 93.4805L64.01 111V75.961L33.67 58.4415V93.4805Z" fill="#5C4EE5"></path> +</svg> diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts index 43d512439f0..978cfeb1aa4 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts @@ -74,7 +74,13 @@ describe("IntegrationsComponent", () => { (integrationList.componentInstance as IntegrationGridComponent).integrations.map( (i) => i.name, ), - ).toEqual(["GitHub Actions", "GitLab CI/CD", "Ansible", "Kubernetes Operator"]); + ).toEqual([ + "GitHub Actions", + "GitLab CI/CD", + "Ansible", + "Kubernetes Operator", + "Terraform Provider", + ]); expect( (sdkList.componentInstance as IntegrationGridComponent).integrations.map((i) => i.name), diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.ts index 31aff308c51..b2279775191 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.ts @@ -36,7 +36,7 @@ export class IntegrationsComponent { }, { name: "Ansible", - linkURL: "https://bitwarden.com/help/ansible-integration/", + linkURL: "https://galaxy.ansible.com/ui/repo/published/bitwarden/secrets", image: "../../../../../../../images/secrets-manager/integrations/ansible.svg", type: IntegrationType.Integration, }, @@ -96,6 +96,13 @@ export class IntegrationsComponent { type: IntegrationType.Integration, newBadgeExpiration: "2024-8-12", }, + { + name: "Terraform Provider", + linkURL: "https://registry.terraform.io/providers/bitwarden/bitwarden-secrets/latest", + image: "../../../../../../../images/secrets-manager/integrations/terraform.svg", + type: IntegrationType.Integration, + newBadgeExpiration: "2025-12-12", // December 12, 2025 + }, ]; } From d5f2c9d5ec234a9cddc577319fcbbd7a14061eb1 Mon Sep 17 00:00:00 2001 From: Mick Letofsky <mletofsky@bitwarden.com> Date: Mon, 27 Oct 2025 16:25:40 +0100 Subject: [PATCH 50/73] Implement reusable Claude code review workflow (#16979) --- CLAUDE.md => .claude/CLAUDE.md | 0 .claude/prompts/review-code.md | 25 +++++++ .github/workflows/review-code.yml | 118 ++---------------------------- .gitignore | 1 - 4 files changed, 32 insertions(+), 112 deletions(-) rename CLAUDE.md => .claude/CLAUDE.md (100%) create mode 100644 .claude/prompts/review-code.md diff --git a/CLAUDE.md b/.claude/CLAUDE.md similarity index 100% rename from CLAUDE.md rename to .claude/CLAUDE.md diff --git a/.claude/prompts/review-code.md b/.claude/prompts/review-code.md new file mode 100644 index 00000000000..4e5f40b2743 --- /dev/null +++ b/.claude/prompts/review-code.md @@ -0,0 +1,25 @@ +Please review this pull request with a focus on: + +- Code quality and best practices +- Potential bugs or issues +- Security implications +- Performance considerations + +Note: The PR branch is already checked out in the current working directory. + +Provide a comprehensive review including: + +- Summary of changes since last review +- Critical issues found (be thorough) +- Suggested improvements (be thorough) +- Good practices observed (be concise - list only the most notable items without elaboration) +- Action items for the author +- Leverage collapsible <details> sections where appropriate for lengthy explanations or code snippets to enhance human readability + +When reviewing subsequent commits: + +- Track status of previously identified issues (fixed/unfixed/reopened) +- Identify NEW problems introduced since last review +- Note if fixes introduced new issues + +IMPORTANT: Be comprehensive about issues and improvements. For good practices, be brief - just note what was done well without explaining why or praising excessively. diff --git a/.github/workflows/review-code.yml b/.github/workflows/review-code.yml index 83cbc3bb547..46309af38ea 100644 --- a/.github/workflows/review-code.yml +++ b/.github/workflows/review-code.yml @@ -1,124 +1,20 @@ -name: Review code +name: Code Review on: pull_request: - types: [opened, synchronize, reopened] + types: [opened, synchronize, reopened, ready_for_review] permissions: {} jobs: review: name: Review - runs-on: ubuntu-24.04 + uses: bitwarden/gh-actions/.github/workflows/_review-code.yml@main + secrets: + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} permissions: contents: read id-token: write pull-requests: write - - steps: - - name: Check out repo - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - with: - fetch-depth: 0 - persist-credentials: false - - - name: Check for Vault team changes - id: check_changes - run: | - # Ensure we have the base branch - git fetch origin ${{ github.base_ref }} - - echo "Comparing changes between origin/${{ github.base_ref }} and HEAD" - CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD) - - if [ -z "$CHANGED_FILES" ]; then - echo "Zero files changed" - echo "vault_team_changes=false" >> $GITHUB_OUTPUT - exit 0 - fi - - # Handle variations in spacing and multiple teams - VAULT_PATTERNS=$(grep -E "@bitwarden/team-vault-dev(\s|$)" .github/CODEOWNERS 2>/dev/null | awk '{print $1}') - - if [ -z "$VAULT_PATTERNS" ]; then - echo "⚠️ No patterns found for @bitwarden/team-vault-dev in CODEOWNERS" - echo "vault_team_changes=false" >> $GITHUB_OUTPUT - exit 0 - fi - - vault_team_changes=false - for pattern in $VAULT_PATTERNS; do - echo "Checking pattern: $pattern" - - # Handle **/directory patterns - if [[ "$pattern" == "**/"* ]]; then - # Remove the **/ prefix - dir_pattern="${pattern#\*\*/}" - # Check if any file contains this directory in its path - if echo "$CHANGED_FILES" | grep -qE "(^|/)${dir_pattern}(/|$)"; then - vault_team_changes=true - echo "✅ Found files matching pattern: $pattern" - echo "$CHANGED_FILES" | grep -E "(^|/)${dir_pattern}(/|$)" | sed 's/^/ - /' - break - fi - else - # Handle other patterns (shouldn't happen based on your CODEOWNERS) - if echo "$CHANGED_FILES" | grep -q "$pattern"; then - vault_team_changes=true - echo "✅ Found files matching pattern: $pattern" - echo "$CHANGED_FILES" | grep "$pattern" | sed 's/^/ - /' - break - fi - fi - done - - echo "vault_team_changes=$vault_team_changes" >> $GITHUB_OUTPUT - - if [ "$vault_team_changes" = "true" ]; then - echo "" - echo "✅ Vault team changes detected - proceeding with review" - else - echo "" - echo "❌ No Vault team changes detected - skipping review" - fi - - - name: Review with Claude Code - if: steps.check_changes.outputs.vault_team_changes == 'true' - uses: anthropics/claude-code-action@ac1a3207f3f00b4a37e2f3a6f0935733c7c64651 # v1.0.11 - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - track_progress: true - use_sticky_comment: true - prompt: | - REPO: ${{ github.repository }} - PR NUMBER: ${{ github.event.pull_request.number }} - TITLE: ${{ github.event.pull_request.title }} - BODY: ${{ github.event.pull_request.body }} - AUTHOR: ${{ github.event.pull_request.user.login }} - COMMIT: ${{ github.event.pull_request.head.sha }} - - Please review this pull request with a focus on: - - Code quality and best practices - - Potential bugs or issues - - Security implications - - Performance considerations - - Note: The PR branch is already checked out in the current working directory. - - Provide a comprehensive review including: - - Summary of changes since last review - - Critical issues found (be thorough) - - Suggested improvements (be thorough) - - Good practices observed (be concise - list only the most notable items without elaboration) - - Action items for the author - - Leverage collapsible <details> sections where appropriate for lengthy explanations or code snippets to enhance human readability - - When reviewing subsequent commits: - - Track status of previously identified issues (fixed/unfixed/reopened) - - Identify NEW problems introduced since last review - - Note if fixes introduced new issues - - IMPORTANT: Be comprehensive about issues and improvements. For good practices, be brief - just note what was done well without explaining why or praising excessively. - - claude_args: | - --allowedTools "mcp__github_comment__update_claude_comment,mcp__github_inline_comment__create_inline_comment,Bash(gh pr diff:*),Bash(gh pr view:*)" diff --git a/.gitignore b/.gitignore index 6b13d22caa7..a88c3bd133b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,6 @@ Thumbs.db *.launch .settings/ *.sublime-workspace -.claude .serena # Visual Studio Code From b3359872132bb4a63199aedc0936d6e6876432b6 Mon Sep 17 00:00:00 2001 From: Kyle Denney <4227399+kdenney@users.noreply.github.com> Date: Mon, 27 Oct 2025 10:44:56 -0500 Subject: [PATCH 51/73] [PM-27267] fix disappearing border from upgrade plan card (#17007) --- .../billing/organizations/change-plan-dialog.component.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts index ac415ac4be2..e2a30dd585c 100644 --- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts @@ -451,9 +451,9 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { "tw-border-solid", "tw-border-primary-600", "hover:tw-border-primary-700", - "focus:tw-border-2", - "focus:tw-border-primary-700", - "focus:tw-rounded-lg", + "tw-border-2", + "!tw-border-primary-700", + "tw-rounded-lg", ]; } case PlanCardState.NotSelected: { From bd89c0ce6de41140a609e7aabb3697c48edd615a Mon Sep 17 00:00:00 2001 From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Date: Mon, 27 Oct 2025 11:04:17 -0500 Subject: [PATCH 52/73] [PM-23628] Require userId for fetching provider keys (#16993) * remove getProviderKey and expose providerKeys$ * update consumers --- .../organization-plans.component.ts | 15 ++- ...-existing-organization-dialog.component.ts | 6 ++ .../dialogs/bulk-confirm-dialog.component.ts | 16 ++- .../providers/manage/members.component.ts | 14 ++- .../services/web-provider.service.spec.ts | 101 ++++++++++++++++-- .../services/web-provider.service.ts | 38 ++++--- .../src/abstractions/key.service.ts | 18 ++-- libs/key-management/src/key.service.spec.ts | 45 +++++++- libs/key-management/src/key.service.ts | 34 ++---- 9 files changed, 223 insertions(+), 64 deletions(-) diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.ts b/apps/web/src/app/billing/organizations/organization-plans.component.ts index a4ebba7a760..7c081b38279 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.ts +++ b/apps/web/src/app/billing/organizations/organization-plans.component.ts @@ -31,6 +31,7 @@ import { ProviderOrganizationCreateRequest } from "@bitwarden/common/admin-conso import { ProviderResponse } from "@bitwarden/common/admin-console/models/response/provider/provider.response"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { assertNonNullish } from "@bitwarden/common/auth/utils"; import { PlanSponsorshipType, PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; import { BillingResponse } from "@bitwarden/common/billing/models/response/billing.response"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; @@ -41,7 +42,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { OrganizationId } from "@bitwarden/common/types/guid"; +import { OrganizationId, ProviderId, UserId } from "@bitwarden/common/types/guid"; import { OrgKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { ToastService } from "@bitwarden/components"; @@ -654,7 +655,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { orgId = this.selfHosted ? await this.createSelfHosted(key, collectionCt, orgKeys) - : await this.createCloudHosted(key, collectionCt, orgKeys, orgKey[1]); + : await this.createCloudHosted(key, collectionCt, orgKeys, orgKey[1], activeUserId); this.toastService.showToast({ variant: "success", @@ -808,6 +809,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { collectionCt: string, orgKeys: [string, EncString], orgKey: SymmetricCryptoKey, + activeUserId: UserId, ): Promise<string> { const request = new OrganizationCreateRequest(); request.key = key; @@ -855,7 +857,14 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { this.formGroup.controls.clientOwnerEmail.value, request, ); - const providerKey = await this.keyService.getProviderKey(this.providerId); + + const providerKey = await firstValueFrom( + this.keyService + .providerKeys$(activeUserId) + .pipe(map((providerKeys) => providerKeys?.[this.providerId as ProviderId] ?? null)), + ); + assertNonNullish(providerKey, "Provider key not found"); + providerRequest.organizationCreateRequest.key = ( await this.encryptService.wrapSymmetricKey(orgKey, providerKey) ).encryptedString; diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/add-existing-organization-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/add-existing-organization-dialog.component.ts index e36e4e5f0c6..8ce8153b36e 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/add-existing-organization-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/add-existing-organization-dialog.component.ts @@ -1,8 +1,11 @@ import { Component, Inject, OnInit } from "@angular/core"; +import { firstValueFrom } from "rxjs"; import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; import { AddableOrganizationResponse } from "@bitwarden/common/admin-console/models/response/addable-organization.response"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { DIALOG_DATA, @@ -46,6 +49,7 @@ export class AddExistingOrganizationDialogComponent implements OnInit { private providerApiService: ProviderApiServiceAbstraction, private toastService: ToastService, private webProviderService: WebProviderService, + private accountService: AccountService, ) {} async ngOnInit() { @@ -57,9 +61,11 @@ export class AddExistingOrganizationDialogComponent implements OnInit { addExistingOrganization = async (): Promise<void> => { if (this.selectedOrganization) { + const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); await this.webProviderService.addOrganizationToProvider( this.dialogParams.provider.id, this.selectedOrganization.id, + userId, ); this.toastService.showToast({ diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts index dd54b842062..7ade77ed01b 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts @@ -1,6 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Component, Inject } from "@angular/core"; +import { firstValueFrom, map, Observable, switchMap } from "rxjs"; import { OrganizationUserBulkPublicKeyResponse, @@ -12,10 +13,14 @@ import { ProviderUserBulkConfirmRequest } from "@bitwarden/common/admin-console/ import { ProviderUserBulkRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk.request"; import { ProviderUserBulkPublicKeyResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk-public-key.response"; import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { ProviderId } from "@bitwarden/common/types/guid"; +import { ProviderKey } from "@bitwarden/common/types/key"; import { DIALOG_DATA, DialogConfig, DialogService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; import { BaseBulkConfirmComponent } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/base-bulk-confirm.component"; @@ -35,6 +40,7 @@ type BulkConfirmDialogParams = { }) export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent { providerId: string; + providerKey$: Observable<ProviderKey>; constructor( private apiService: ApiService, @@ -42,15 +48,21 @@ export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent { protected encryptService: EncryptService, @Inject(DIALOG_DATA) protected dialogParams: BulkConfirmDialogParams, protected i18nService: I18nService, + private accountService: AccountService, ) { super(keyService, encryptService, i18nService); this.providerId = dialogParams.providerId; + this.providerKey$ = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.keyService.providerKeys$(userId)), + map((providerKeysById) => providerKeysById?.[this.providerId as ProviderId]), + ); this.users = dialogParams.users; } - protected getCryptoKey = (): Promise<SymmetricCryptoKey> => - this.keyService.getProviderKey(this.providerId); + protected getCryptoKey = async (): Promise<SymmetricCryptoKey> => + await firstValueFrom(this.providerKey$); protected getPublicKeys = async (): Promise< ListResponse<OrganizationUserBulkPublicKeyResponse | ProviderUserBulkPublicKeyResponse> 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 b1cd52cf8a6..268a82ac12f 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 @@ -4,7 +4,7 @@ import { Component } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute, Router } from "@angular/router"; import { combineLatest, firstValueFrom, lastValueFrom, switchMap } from "rxjs"; -import { first } from "rxjs/operators"; +import { first, map } from "rxjs/operators"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -16,11 +16,13 @@ import { ProviderUserConfirmRequest } from "@bitwarden/common/admin-console/mode import { ProviderUserUserDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user.response"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { assertNonNullish } from "@bitwarden/common/auth/utils"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; +import { ProviderId } from "@bitwarden/common/types/guid"; import { DialogRef, DialogService, ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; import { BaseMembersComponent } from "@bitwarden/web-vault/app/admin-console/common/base-members.component"; @@ -204,7 +206,15 @@ export class MembersComponent extends BaseMembersComponent<ProviderUser> { async confirmUser(user: ProviderUser, publicKey: Uint8Array): Promise<MemberActionResult> { try { - const providerKey = await this.keyService.getProviderKey(this.providerId); + const providerKey = await firstValueFrom( + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.keyService.providerKeys$(userId)), + map((providerKeys) => providerKeys?.[this.providerId as ProviderId] ?? null), + ), + ); + assertNonNullish(providerKey, "Provider key not found"); + const key = await this.encryptService.encapsulateKeyUnsigned(providerKey, publicKey); const request = new ProviderUserConfirmRequest(); request.key = key.encryptedString; diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.spec.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.spec.ts index b2da18dd047..2accd760fcb 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.spec.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.spec.ts @@ -1,4 +1,5 @@ import { MockProxy, mock } from "jest-mock-extended"; +import { of } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; @@ -8,7 +9,6 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { StateProvider } from "@bitwarden/common/platform/state"; import { OrgKey, ProviderKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { newGuid } from "@bitwarden/guid"; @@ -24,16 +24,22 @@ describe("WebProviderService", () => { let apiService: MockProxy<ApiService>; let i18nService: MockProxy<I18nService>; let encryptService: MockProxy<EncryptService>; - let stateProvider: MockProxy<StateProvider>; let providerApiService: MockProxy<ProviderApiServiceAbstraction>; + const activeUserId = newGuid() as UserId; + const providerId = "provider-123"; + const mockOrgKey = new SymmetricCryptoKey(new Uint8Array(64)) as OrgKey; + const mockProviderKey = new SymmetricCryptoKey(new Uint8Array(64)) as ProviderKey; + const mockProviderKeysById: Record<string, ProviderKey> = { + [providerId]: mockProviderKey, + }; + beforeEach(() => { keyService = mock(); syncService = mock(); apiService = mock(); i18nService = mock(); encryptService = mock(); - stateProvider = mock(); providerApiService = mock(); sut = new WebProviderService( @@ -42,14 +48,69 @@ describe("WebProviderService", () => { apiService, i18nService, encryptService, - stateProvider, providerApiService, ); }); + describe("addOrganizationToProvider", () => { + const organizationId = "org-789"; + const encryptedOrgKey = new EncString("encrypted-org-key"); + const mockOrgKeysById: Record<string, OrgKey> = { + [organizationId]: mockOrgKey, + }; + + beforeEach(() => { + keyService.orgKeys$.mockReturnValue(of(mockOrgKeysById)); + keyService.providerKeys$.mockReturnValue(of(mockProviderKeysById)); + encryptService.wrapSymmetricKey.mockResolvedValue(encryptedOrgKey); + }); + + it("adds an organization to a provider with correct encryption", async () => { + await sut.addOrganizationToProvider(providerId, organizationId, activeUserId); + + expect(keyService.orgKeys$).toHaveBeenCalledWith(activeUserId); + expect(keyService.providerKeys$).toHaveBeenCalledWith(activeUserId); + expect(encryptService.wrapSymmetricKey).toHaveBeenCalledWith(mockOrgKey, mockProviderKey); + expect(providerApiService.addOrganizationToProvider).toHaveBeenCalledWith(providerId, { + key: encryptedOrgKey.encryptedString, + organizationId, + }); + expect(syncService.fullSync).toHaveBeenCalledWith(true); + }); + + it("throws an error if organization key is not found", async () => { + const invalidOrgId = "invalid-org"; + + await expect( + sut.addOrganizationToProvider(providerId, invalidOrgId, activeUserId), + ).rejects.toThrow("Organization key not found"); + }); + + it("throws an error if no organization keys are available", async () => { + keyService.orgKeys$.mockReturnValue(of(null)); + + await expect( + sut.addOrganizationToProvider(providerId, organizationId, activeUserId), + ).rejects.toThrow("Organization key not found"); + }); + + it("throws an error if provider key is not found", async () => { + const invalidProviderId = "invalid-provider"; + await expect( + sut.addOrganizationToProvider(invalidProviderId, organizationId, activeUserId), + ).rejects.toThrow("Provider key not found"); + }); + + it("throws an error if no provider keys are available", async () => { + keyService.providerKeys$.mockReturnValue(of(null)); + + await expect( + sut.addOrganizationToProvider(providerId, organizationId, activeUserId), + ).rejects.toThrow("Provider key not found"); + }); + }); + describe("createClientOrganization", () => { - const activeUserId = newGuid() as UserId; - const providerId = "provider-123"; const name = "Test Org"; const ownerEmail = "owner@example.com"; const planType = PlanType.EnterpriseAnnually; @@ -59,15 +120,13 @@ describe("WebProviderService", () => { const encryptedProviderKey = new EncString("encrypted-provider-key"); const encryptedCollectionName = new EncString("encrypted-collection-name"); const defaultCollectionTranslation = "Default Collection"; - const mockOrgKey = new SymmetricCryptoKey(new Uint8Array(64)) as OrgKey; - const mockProviderKey = new SymmetricCryptoKey(new Uint8Array(64)) as ProviderKey; beforeEach(() => { keyService.makeOrgKey.mockResolvedValue([new EncString("mockEncryptedKey"), mockOrgKey]); keyService.makeKeyPair.mockResolvedValue([publicKey, encryptedPrivateKey]); i18nService.t.mockReturnValue(defaultCollectionTranslation); encryptService.encryptString.mockResolvedValue(encryptedCollectionName); - keyService.getProviderKey.mockResolvedValue(mockProviderKey); + keyService.providerKeys$.mockReturnValue(of(mockProviderKeysById)); encryptService.wrapSymmetricKey.mockResolvedValue(encryptedProviderKey); }); @@ -88,7 +147,7 @@ describe("WebProviderService", () => { defaultCollectionTranslation, mockOrgKey, ); - expect(keyService.getProviderKey).toHaveBeenCalledWith(providerId); + expect(keyService.providerKeys$).toHaveBeenCalledWith(activeUserId); expect(encryptService.wrapSymmetricKey).toHaveBeenCalledWith(mockOrgKey, mockProviderKey); expect(providerApiService.createProviderOrganization).toHaveBeenCalledWith( @@ -107,5 +166,27 @@ describe("WebProviderService", () => { expect(apiService.refreshIdentityToken).toHaveBeenCalled(); expect(syncService.fullSync).toHaveBeenCalledWith(true); }); + + it("throws an error if provider key is not found", async () => { + const invalidProviderId = "invalid-provider"; + await expect( + sut.createClientOrganization( + invalidProviderId, + name, + ownerEmail, + planType, + seats, + activeUserId, + ), + ).rejects.toThrow("Provider key not found"); + }); + + it("throws an error if no provider keys are available", async () => { + keyService.providerKeys$.mockReturnValue(of(null)); + + await expect( + sut.createClientOrganization(providerId, name, ownerEmail, planType, seats, activeUserId), + ).rejects.toThrow("Provider key not found"); + }); }); }); diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts index 78931f9c445..e1eea78d26a 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts @@ -1,18 +1,17 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Injectable } from "@angular/core"; -import { firstValueFrom, map } from "rxjs"; -import { switchMap } from "rxjs/operators"; +import { combineLatest, firstValueFrom, map } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { CreateProviderOrganizationRequest } from "@bitwarden/common/admin-console/models/request/create-provider-organization.request"; import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; +import { assertNonNullish } from "@bitwarden/common/auth/utils"; import { PlanType } from "@bitwarden/common/billing/enums"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { StateProvider } from "@bitwarden/common/platform/state"; -import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; +import { OrganizationId, ProviderId, UserId } from "@bitwarden/common/types/guid"; import { OrgKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { KeyService } from "@bitwarden/key-management"; @@ -25,18 +24,26 @@ export class WebProviderService { private apiService: ApiService, private i18nService: I18nService, private encryptService: EncryptService, - private stateProvider: StateProvider, private providerApiService: ProviderApiServiceAbstraction, ) {} - async addOrganizationToProvider(providerId: string, organizationId: string): Promise<void> { - const orgKey = await firstValueFrom( - this.stateProvider.activeUserId$.pipe( - switchMap((userId) => this.keyService.orgKeys$(userId)), - map((organizationKeysById) => organizationKeysById[organizationId as OrganizationId]), - ), + async addOrganizationToProvider( + providerId: string, + organizationId: string, + activeUserId: UserId, + ): Promise<void> { + const [orgKeysById, providerKeys] = await firstValueFrom( + combineLatest([ + this.keyService.orgKeys$(activeUserId), + this.keyService.providerKeys$(activeUserId), + ]), ); - const providerKey = await this.keyService.getProviderKey(providerId); + + const orgKey = orgKeysById?.[organizationId as OrganizationId]; + const providerKey = providerKeys?.[providerId as ProviderId]; + assertNonNullish(orgKey, "Organization key not found"); + assertNonNullish(providerKey, "Provider key not found"); + const encryptedOrgKey = await this.encryptService.wrapSymmetricKey(orgKey, providerKey); await this.providerApiService.addOrganizationToProvider(providerId, { key: encryptedOrgKey.encryptedString, @@ -62,7 +69,12 @@ export class WebProviderService { organizationKey, ); - const providerKey = await this.keyService.getProviderKey(providerId); + const providerKey = await firstValueFrom( + this.keyService + .providerKeys$(activeUserId) + .pipe(map((providerKeys) => providerKeys?.[providerId as ProviderId])), + ); + assertNonNullish(providerKey, "Provider key not found"); const encryptedProviderKey = await this.encryptService.wrapSymmetricKey( organizationKey, diff --git a/libs/key-management/src/abstractions/key.service.ts b/libs/key-management/src/abstractions/key.service.ts index abd4dcc1563..7891c9952b2 100644 --- a/libs/key-management/src/abstractions/key.service.ts +++ b/libs/key-management/src/abstractions/key.service.ts @@ -10,7 +10,7 @@ import { import { WrappedSigningKey } from "@bitwarden/common/key-management/types"; import { KeySuffixOptions, HashPurpose } from "@bitwarden/common/platform/enums"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; +import { OrganizationId, ProviderId, UserId } from "@bitwarden/common/types/guid"; import { UserKey, MasterKey, @@ -248,17 +248,19 @@ export abstract class KeyService { /** * Stores the provider keys for a given user. - * @param orgs The provider orgs for which to save the keys from. + * @param providers The provider orgs for which to save the keys from. * @param userId The user id of the user for which to store the keys for. */ - abstract setProviderKeys(orgs: ProfileProviderResponse[], userId: UserId): Promise<void>; + abstract setProviderKeys(providers: ProfileProviderResponse[], userId: UserId): Promise<void>; + /** - * - * @throws Error when providerId is null or no active user - * @param providerId The desired provider - * @returns The provider's symmetric key + * Gets an observable of provider keys for the given user. + * @param userId The user to get provider keys for. + * @return An observable stream of the users providers keys if they are unlocked, or null if the user is not unlocked. + * @throws If an invalid user id is passed in. */ - abstract getProviderKey(providerId: string): Promise<ProviderKey | null>; + abstract providerKeys$(userId: UserId): Observable<Record<ProviderId, ProviderKey> | null>; + /** * Creates a new organization key and encrypts it with the user's public key. * This method can also return Provider keys for creating new Provider users. diff --git a/libs/key-management/src/key.service.spec.ts b/libs/key-management/src/key.service.spec.ts index 0dd9f3603f5..5d5340d4900 100644 --- a/libs/key-management/src/key.service.spec.ts +++ b/libs/key-management/src/key.service.spec.ts @@ -39,7 +39,7 @@ import { FakeSingleUserState, } from "@bitwarden/common/spec"; import { CsprngArray } from "@bitwarden/common/types/csprng"; -import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; +import { OrganizationId, ProviderId, UserId } from "@bitwarden/common/types/guid"; import { UserKey, MasterKey, @@ -1314,6 +1314,49 @@ describe("keyService", () => { }); }); + describe("providerKeys$", () => { + let mockUserPrivateKey: Uint8Array; + let mockProviderKeys: Record<ProviderId, ProviderKey>; + + beforeEach(() => { + mockUserPrivateKey = makeStaticByteArray(64, 1); + mockProviderKeys = { + ["provider1" as ProviderId]: makeSymmetricCryptoKey<ProviderKey>(64), + ["provider2" as ProviderId]: makeSymmetricCryptoKey<ProviderKey>(64), + }; + }); + + it("returns null when userPrivateKey is null", async () => { + jest.spyOn(keyService, "userPrivateKey$").mockReturnValue(of(null)); + + const result = await firstValueFrom(keyService.providerKeys$(mockUserId)); + + expect(result).toBeNull(); + }); + + it("returns provider keys when userPrivateKey is available", async () => { + jest.spyOn(keyService, "userPrivateKey$").mockReturnValue(of(mockUserPrivateKey as any)); + jest.spyOn(keyService as any, "providerKeysHelper$").mockReturnValue(of(mockProviderKeys)); + + const result = await firstValueFrom(keyService.providerKeys$(mockUserId)); + + expect(result).toEqual(mockProviderKeys); + expect((keyService as any).providerKeysHelper$).toHaveBeenCalledWith( + mockUserId, + mockUserPrivateKey, + ); + }); + + it("returns null when providerKeysHelper$ returns null", async () => { + jest.spyOn(keyService, "userPrivateKey$").mockReturnValue(of(mockUserPrivateKey as any)); + jest.spyOn(keyService as any, "providerKeysHelper$").mockReturnValue(of(null)); + + const result = await firstValueFrom(keyService.providerKeys$(mockUserId)); + + expect(result).toBeNull(); + }); + }); + describe("makeKeyPair", () => { test.each([null as unknown as SymmetricCryptoKey, undefined as unknown as SymmetricCryptoKey])( "throws when the provided key is %s", diff --git a/libs/key-management/src/key.service.ts b/libs/key-management/src/key.service.ts index fc340410124..032faeaf42e 100644 --- a/libs/key-management/src/key.service.ts +++ b/libs/key-management/src/key.service.ts @@ -426,20 +426,16 @@ export class DefaultKeyService implements KeyServiceAbstraction { }); } - // TODO: Deprecate in favor of observable - async getProviderKey(providerId: ProviderId): Promise<ProviderKey | null> { - if (providerId == null) { - return null; - } + providerKeys$(userId: UserId): Observable<Record<ProviderId, ProviderKey> | null> { + return this.userPrivateKey$(userId).pipe( + switchMap((userPrivateKey) => { + if (userPrivateKey == null) { + return of(null); + } - const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$); - if (activeUserId == null) { - throw new Error("No active user found."); - } - - const providerKeys = await firstValueFrom(this.providerKeys$(activeUserId)); - - return providerKeys?.[providerId] ?? null; + return this.providerKeysHelper$(userId, userPrivateKey); + }), + ); } private async clearProviderKeys(userId: UserId): Promise<void> { @@ -829,18 +825,6 @@ export class DefaultKeyService implements KeyServiceAbstraction { )) as UserPrivateKey; } - providerKeys$(userId: UserId) { - return this.userPrivateKey$(userId).pipe( - switchMap((userPrivateKey) => { - if (userPrivateKey == null) { - return of(null); - } - - return this.providerKeysHelper$(userId, userPrivateKey); - }), - ); - } - /** * A helper for decrypting provider keys that requires a user id and that users decrypted private key * this is helpful for when you may have already grabbed the user private key and don't want to redo From 42377a1533903c7f82dcb51d8837b09ae64bf950 Mon Sep 17 00:00:00 2001 From: Oscar Hinton <Hinton@users.noreply.github.com> Date: Mon, 27 Oct 2025 17:24:50 +0100 Subject: [PATCH 53/73] [PM-27341] Chrome importer refactors (#16720) Various refactors to the chrome importer --- .github/CODEOWNERS | 2 +- apps/desktop/desktop_native/Cargo.lock | 54 +++++++++---------- apps/desktop/desktop_native/Cargo.toml | 2 +- .../bitwarden_chromium_importer/src/crypto.rs | 48 ----------------- .../bitwarden_chromium_importer/src/lib.rs | 8 --- .../Cargo.toml | 5 +- .../README.md | 23 +++++--- .../src/chromium/mod.rs} | 48 +++++++---------- .../src/chromium/platform}/linux.rs | 4 +- .../src/chromium/platform}/macos.rs | 4 +- .../src/chromium/platform/mod.rs | 7 +++ .../src/chromium/platform}/windows.rs | 5 +- .../chromium_importer/src/lib.rs | 5 ++ .../src/metadata.rs | 9 ++-- .../src/util.rs | 54 +++++++++++-------- apps/desktop/desktop_native/napi/Cargo.toml | 2 +- apps/desktop/desktop_native/napi/index.d.ts | 14 ++--- apps/desktop/desktop_native/napi/src/lib.rs | 42 +++++++++++---- .../import/desktop-import-metadata.service.ts | 6 ++- apps/desktop/src/app/tools/preload.ts | 4 +- 20 files changed, 161 insertions(+), 185 deletions(-) delete mode 100644 apps/desktop/desktop_native/bitwarden_chromium_importer/src/crypto.rs delete mode 100644 apps/desktop/desktop_native/bitwarden_chromium_importer/src/lib.rs rename apps/desktop/desktop_native/{bitwarden_chromium_importer => chromium_importer}/Cargo.toml (92%) rename apps/desktop/desktop_native/{bitwarden_chromium_importer => chromium_importer}/README.md (94%) rename apps/desktop/desktop_native/{bitwarden_chromium_importer/src/chromium.rs => chromium_importer/src/chromium/mod.rs} (89%) rename apps/desktop/desktop_native/{bitwarden_chromium_importer/src => chromium_importer/src/chromium/platform}/linux.rs (97%) rename apps/desktop/desktop_native/{bitwarden_chromium_importer/src => chromium_importer/src/chromium/platform}/macos.rs (97%) create mode 100644 apps/desktop/desktop_native/chromium_importer/src/chromium/platform/mod.rs rename apps/desktop/desktop_native/{bitwarden_chromium_importer/src => chromium_importer/src/chromium/platform}/windows.rs (97%) create mode 100644 apps/desktop/desktop_native/chromium_importer/src/lib.rs rename apps/desktop/desktop_native/{bitwarden_chromium_importer => chromium_importer}/src/metadata.rs (96%) rename apps/desktop/desktop_native/{bitwarden_chromium_importer => chromium_importer}/src/util.rs (77%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f784f375086..8affac3387b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -30,7 +30,7 @@ libs/common/src/auth @bitwarden/team-auth-dev apps/browser/src/tools @bitwarden/team-tools-dev apps/cli/src/tools @bitwarden/team-tools-dev apps/desktop/src/app/tools @bitwarden/team-tools-dev -apps/desktop/desktop_native/bitwarden_chromium_importer @bitwarden/team-tools-dev +apps/desktop/desktop_native/chromium_importer @bitwarden/team-tools-dev apps/web/src/app/tools @bitwarden/team-tools-dev libs/angular/src/tools @bitwarden/team-tools-dev libs/common/src/models/export @bitwarden/team-tools-dev diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 5e658546671..a0cd1b3dcbf 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -440,33 +440,6 @@ dependencies = [ "tokio-util", ] -[[package]] -name = "bitwarden_chromium_importer" -version = "0.0.0" -dependencies = [ - "aes", - "aes-gcm", - "anyhow", - "async-trait", - "base64", - "cbc", - "hex", - "homedir", - "napi", - "napi-derive", - "oo7", - "pbkdf2", - "rand 0.9.1", - "rusqlite", - "security-framework", - "serde", - "serde_json", - "sha1", - "tokio", - "winapi", - "windows 0.61.1", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -606,6 +579,31 @@ dependencies = [ "zeroize", ] +[[package]] +name = "chromium_importer" +version = "0.0.0" +dependencies = [ + "aes", + "aes-gcm", + "anyhow", + "async-trait", + "base64", + "cbc", + "hex", + "homedir", + "oo7", + "pbkdf2", + "rand 0.9.1", + "rusqlite", + "security-framework", + "serde", + "serde_json", + "sha1", + "tokio", + "winapi", + "windows 0.61.1", +] + [[package]] name = "cipher" version = "0.4.4" @@ -968,7 +966,7 @@ dependencies = [ "anyhow", "autotype", "base64", - "bitwarden_chromium_importer", + "chromium_importer", "desktop_core", "hex", "napi", diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index 2168eaa0068..6a366316328 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -2,7 +2,7 @@ resolver = "2" members = [ "autotype", - "bitwarden_chromium_importer", + "chromium_importer", "core", "macos_provider", "napi", diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/crypto.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/crypto.rs deleted file mode 100644 index e6442e21742..00000000000 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/crypto.rs +++ /dev/null @@ -1,48 +0,0 @@ -//! Cryptographic primitives used in the SDK - -use anyhow::{anyhow, Result}; - -use aes::cipher::{ - block_padding::Pkcs7, generic_array::GenericArray, typenum::U32, BlockDecryptMut, KeyIvInit, -}; - -pub fn decrypt_aes256(iv: &[u8; 16], data: &[u8], key: GenericArray<u8, U32>) -> Result<Vec<u8>> { - let iv = GenericArray::from_slice(iv); - let mut data = data.to_vec(); - cbc::Decryptor::<aes::Aes256>::new(&key, iv) - .decrypt_padded_mut::<Pkcs7>(&mut data) - .map_err(|_| anyhow!("Failed to decrypt data"))?; - - Ok(data) -} - -#[cfg(test)] -mod tests { - use aes::cipher::{ - generic_array::{sequence::GenericSequence, GenericArray}, - ArrayLength, - }; - use base64::{engine::general_purpose::STANDARD, Engine}; - - pub fn generate_vec(length: usize, offset: u8, increment: u8) -> Vec<u8> { - (0..length).map(|i| offset + i as u8 * increment).collect() - } - pub fn generate_generic_array<N: ArrayLength<u8>>( - offset: u8, - increment: u8, - ) -> GenericArray<u8, N> { - GenericArray::generate(|i| offset + i as u8 * increment) - } - - #[test] - fn test_decrypt_aes256() { - let iv = generate_vec(16, 0, 1); - let iv: &[u8; 16] = iv.as_slice().try_into().unwrap(); - let key = generate_generic_array(0, 1); - let data: Vec<u8> = STANDARD.decode("ByUF8vhyX4ddU9gcooznwA==").unwrap(); - - let decrypted = super::decrypt_aes256(iv, &data, key).unwrap(); - - assert_eq!(String::from_utf8(decrypted).unwrap(), "EncryptMe!\u{6}\u{6}\u{6}\u{6}\u{6}\u{6}"); - } -} diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/lib.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/lib.rs deleted file mode 100644 index 84f140d2341..00000000000 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/lib.rs +++ /dev/null @@ -1,8 +0,0 @@ -#[macro_use] -extern crate napi_derive; - -pub mod chromium; -pub mod metadata; -pub mod util; - -pub use crate::chromium::platform::SUPPORTED_BROWSERS as PLATFORM_SUPPORTED_BROWSERS; diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml b/apps/desktop/desktop_native/chromium_importer/Cargo.toml similarity index 92% rename from apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml rename to apps/desktop/desktop_native/chromium_importer/Cargo.toml index 656c3ad1504..648a36543c2 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml +++ b/apps/desktop/desktop_native/chromium_importer/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "bitwarden_chromium_importer" +name = "chromium_importer" edition = { workspace = true } license = { workspace = true } version = { workspace = true } @@ -14,8 +14,6 @@ base64 = { workspace = true } cbc = { workspace = true, features = ["alloc"] } hex = { workspace = true } homedir = { workspace = true } -napi = { workspace = true } -napi-derive = { workspace = true } pbkdf2 = "=0.12.2" rand = { workspace = true } rusqlite = { version = "=0.37.0", features = ["bundled"] } @@ -36,4 +34,3 @@ oo7 = { workspace = true } [lints] workspace = true - diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/README.md b/apps/desktop/desktop_native/chromium_importer/README.md similarity index 94% rename from apps/desktop/desktop_native/bitwarden_chromium_importer/README.md rename to apps/desktop/desktop_native/chromium_importer/README.md index 498dd3ac67d..dd563697e5b 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/README.md +++ b/apps/desktop/desktop_native/chromium_importer/README.md @@ -1,6 +1,13 @@ -# Windows ABE Architecture +# Chromium Direct Importer -## Overview +A rust library that allows you to directly import credentials from Chromium-based browsers. + +## Windows ABE Architecture + +On Windows chrome has additional protection measurements which needs to be circumvented in order to +get access to the passwords. + +### Overview The Windows Application Bound Encryption (ABE) consists of three main components that work together: @@ -10,7 +17,7 @@ The Windows Application Bound Encryption (ABE) consists of three main components _(The names of the binaries will be changed for the released product.)_ -## The goal +### The goal The goal of this subsystem is to decrypt the master encryption key with which the login information is encrypted on the local system in Windows. This applies to the most recent versions of Chrome and @@ -24,7 +31,7 @@ Protection API at the system level on top of that. This triply encrypted key is The next paragraphs describe what is done at each level to decrypt the key. -## 1. Client library +### 1. Client library This is a Rust module that is part of the Chromium importer. It only compiles and runs on Windows (see `abe.rs` and `abe_config.rs`). Its main task is to launch `admin.exe` with elevated privileges @@ -52,7 +59,7 @@ admin.exe --service-exe "c:\temp\service.exe" --encrypted "QVBQQgEAAADQjJ3fARXRE **At this point, the user must permit the action to be performed on the UAC screen.** -## 2. Admin executable +### 2. Admin executable This executable receives the full path of `service.exe` and the data to be decrypted. @@ -67,7 +74,7 @@ is sent to the named pipe server created by the user. The user responds with `ok After that, the executable stops and uninstalls the service and then exits. -## 3. System service +### 3. System service The service starts and creates a named pipe server for communication between `admin.exe` and the system service. Please note that it is not possible to communicate between the user and the system @@ -83,7 +90,7 @@ removed from the system. Even though we send only one request, the service is de many clients with as many messages as needed and could be installed on the system permanently if necessary. -## 4. Back to client library +### 4. Back to client library The decrypted base64-encoded string comes back from the admin executable to the named pipe server at the user level. At this point, it has been decrypted only once at the system level. @@ -99,7 +106,7 @@ itself), it's either AES-256-GCM or ChaCha20Poly1305 encryption scheme. The deta After all of these steps, we have the master key which can be used to decrypt the password information stored in the local database. -## Summary +### Summary The Windows ABE decryption process involves a three-tier architecture with named pipe communication: diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/chromium.rs b/apps/desktop/desktop_native/chromium_importer/src/chromium/mod.rs similarity index 89% rename from apps/desktop/desktop_native/bitwarden_chromium_importer/src/chromium.rs rename to apps/desktop/desktop_native/chromium_importer/src/chromium/mod.rs index 094500e6d42..55728460436 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/chromium.rs +++ b/apps/desktop/desktop_native/chromium_importer/src/chromium/mod.rs @@ -7,11 +7,9 @@ use hex::decode; use homedir::my_home; use rusqlite::{params, Connection}; -// Platform-specific code -#[cfg_attr(target_os = "linux", path = "linux.rs")] -#[cfg_attr(target_os = "windows", path = "windows.rs")] -#[cfg_attr(target_os = "macos", path = "macos.rs")] -pub mod platform; +mod platform; + +pub(crate) use platform::SUPPORTED_BROWSERS as PLATFORM_SUPPORTED_BROWSERS; // // Public API @@ -22,10 +20,7 @@ pub struct ProfileInfo { pub name: String, pub folder: String, - #[allow(dead_code)] pub account_name: Option<String>, - - #[allow(dead_code)] pub account_email: Option<String>, } @@ -113,12 +108,12 @@ pub async fn import_logins( // #[derive(Debug, Clone, Copy)] -pub struct BrowserConfig { +pub(crate) struct BrowserConfig { pub name: &'static str, pub data_dir: &'static str, } -pub static SUPPORTED_BROWSER_MAP: LazyLock< +pub(crate) static SUPPORTED_BROWSER_MAP: LazyLock< std::collections::HashMap<&'static str, &'static BrowserConfig>, > = LazyLock::new(|| { platform::SUPPORTED_BROWSERS @@ -140,12 +135,12 @@ fn get_browser_data_dir(config: &BrowserConfig) -> Result<PathBuf> { // #[async_trait] -pub trait CryptoService: Send { +pub(crate) trait CryptoService: Send { async fn decrypt_to_string(&mut self, encrypted: &[u8]) -> Result<String>; } #[derive(serde::Deserialize, Clone)] -pub struct LocalState { +pub(crate) struct LocalState { profile: AllProfiles, #[allow(dead_code)] os_crypt: Option<OsCrypt>, @@ -198,16 +193,17 @@ fn load_local_state(browser_dir: &Path) -> Result<LocalState> { } fn get_profile_info(local_state: &LocalState) -> Vec<ProfileInfo> { - let mut profile_infos = Vec::new(); - for (name, info) in local_state.profile.info_cache.iter() { - profile_infos.push(ProfileInfo { + local_state + .profile + .info_cache + .iter() + .map(|(name, info)| ProfileInfo { name: info.name.clone(), folder: name.clone(), account_name: info.gaia_name.clone(), account_email: info.user_name.clone(), - }); - } - profile_infos + }) + .collect() } struct EncryptedLogin { @@ -264,17 +260,16 @@ fn hex_to_bytes(hex: &str) -> Vec<u8> { decode(hex).unwrap_or_default() } -fn does_table_exist(conn: &Connection, table_name: &str) -> Result<bool, rusqlite::Error> { - let mut stmt = conn.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?1")?; - let exists = stmt.exists(params![table_name])?; - Ok(exists) +fn table_exist(conn: &Connection, table_name: &str) -> Result<bool, rusqlite::Error> { + conn.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?1")? + .exists(params![table_name]) } fn query_logins(db_path: &str) -> Result<Vec<EncryptedLogin>, rusqlite::Error> { let conn = Connection::open(db_path)?; - let have_logins = does_table_exist(&conn, "logins")?; - let have_password_notes = does_table_exist(&conn, "password_notes")?; + let have_logins = table_exist(&conn, "logins")?; + let have_password_notes = table_exist(&conn, "password_notes")?; if !have_logins || !have_password_notes { return Ok(vec![]); } @@ -308,10 +303,7 @@ fn query_logins(db_path: &str) -> Result<Vec<EncryptedLogin>, rusqlite::Error> { }) })?; - let mut logins = Vec::new(); - for login in logins_iter { - logins.push(login?); - } + let logins = logins_iter.collect::<Result<Vec<_>, _>>()?; Ok(logins) } diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/linux.rs b/apps/desktop/desktop_native/chromium_importer/src/chromium/platform/linux.rs similarity index 97% rename from apps/desktop/desktop_native/bitwarden_chromium_importer/src/linux.rs rename to apps/desktop/desktop_native/chromium_importer/src/chromium/platform/linux.rs index be3bcdb1e1d..227dffdcca7 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/linux.rs +++ b/apps/desktop/desktop_native/chromium_importer/src/chromium/platform/linux.rs @@ -13,7 +13,7 @@ use crate::util; // // TODO: It's possible that there might be multiple possible data directories, depending on the installation method (e.g., snap, flatpak, etc.). -pub const SUPPORTED_BROWSERS: [BrowserConfig; 4] = [ +pub(crate) const SUPPORTED_BROWSERS: &[BrowserConfig] = &[ BrowserConfig { name: "Chrome", data_dir: ".config/google-chrome", @@ -32,7 +32,7 @@ pub const SUPPORTED_BROWSERS: [BrowserConfig; 4] = [ }, ]; -pub fn get_crypto_service( +pub(crate) fn get_crypto_service( browser_name: &String, _local_state: &LocalState, ) -> Result<Box<dyn CryptoService>> { diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/macos.rs b/apps/desktop/desktop_native/chromium_importer/src/chromium/platform/macos.rs similarity index 97% rename from apps/desktop/desktop_native/bitwarden_chromium_importer/src/macos.rs rename to apps/desktop/desktop_native/chromium_importer/src/chromium/platform/macos.rs index bcb2c005000..c0e770c161b 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/macos.rs +++ b/apps/desktop/desktop_native/chromium_importer/src/chromium/platform/macos.rs @@ -10,7 +10,7 @@ use crate::util; // Public API // -pub const SUPPORTED_BROWSERS: [BrowserConfig; 7] = [ +pub(crate) const SUPPORTED_BROWSERS: &[BrowserConfig] = &[ BrowserConfig { name: "Chrome", data_dir: "Library/Application Support/Google/Chrome", @@ -41,7 +41,7 @@ pub const SUPPORTED_BROWSERS: [BrowserConfig; 7] = [ }, ]; -pub fn get_crypto_service( +pub(crate) fn get_crypto_service( browser_name: &String, _local_state: &LocalState, ) -> Result<Box<dyn CryptoService>> { diff --git a/apps/desktop/desktop_native/chromium_importer/src/chromium/platform/mod.rs b/apps/desktop/desktop_native/chromium_importer/src/chromium/platform/mod.rs new file mode 100644 index 00000000000..2a21ef23d82 --- /dev/null +++ b/apps/desktop/desktop_native/chromium_importer/src/chromium/platform/mod.rs @@ -0,0 +1,7 @@ +// Platform-specific code +#[cfg_attr(target_os = "linux", path = "linux.rs")] +#[cfg_attr(target_os = "windows", path = "windows.rs")] +#[cfg_attr(target_os = "macos", path = "macos.rs")] +mod native; + +pub(crate) use native::*; diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs b/apps/desktop/desktop_native/chromium_importer/src/chromium/platform/windows.rs similarity index 97% rename from apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs rename to apps/desktop/desktop_native/chromium_importer/src/chromium/platform/windows.rs index 096808aafb6..79c462c29a1 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs +++ b/apps/desktop/desktop_native/chromium_importer/src/chromium/platform/windows.rs @@ -15,8 +15,7 @@ use crate::util; // Public API // -// IMPORTANT adjust array size when enabling / disabling chromium importers here -pub const SUPPORTED_BROWSERS: [BrowserConfig; 6] = [ +pub(crate) const SUPPORTED_BROWSERS: &[BrowserConfig] = &[ BrowserConfig { name: "Brave", data_dir: "AppData/Local/BraveSoftware/Brave-Browser/User Data", @@ -43,7 +42,7 @@ pub const SUPPORTED_BROWSERS: [BrowserConfig; 6] = [ }, ]; -pub fn get_crypto_service( +pub(crate) fn get_crypto_service( _browser_name: &str, local_state: &LocalState, ) -> Result<Box<dyn CryptoService>> { diff --git a/apps/desktop/desktop_native/chromium_importer/src/lib.rs b/apps/desktop/desktop_native/chromium_importer/src/lib.rs new file mode 100644 index 00000000000..d92515c39f9 --- /dev/null +++ b/apps/desktop/desktop_native/chromium_importer/src/lib.rs @@ -0,0 +1,5 @@ +#![doc = include_str!("../README.md")] + +pub mod chromium; +pub mod metadata; +mod util; diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/metadata.rs b/apps/desktop/desktop_native/chromium_importer/src/metadata.rs similarity index 96% rename from apps/desktop/desktop_native/bitwarden_chromium_importer/src/metadata.rs rename to apps/desktop/desktop_native/chromium_importer/src/metadata.rs index 28f13cd9863..bfd7f184621 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/metadata.rs +++ b/apps/desktop/desktop_native/chromium_importer/src/metadata.rs @@ -1,8 +1,7 @@ use std::collections::{HashMap, HashSet}; -use crate::{chromium::InstalledBrowserRetriever, PLATFORM_SUPPORTED_BROWSERS}; +use crate::chromium::{InstalledBrowserRetriever, PLATFORM_SUPPORTED_BROWSERS}; -#[napi(object)] /// Mechanisms that load data into the importer pub struct NativeImporterMetadata { /// Identifies the importer @@ -24,7 +23,7 @@ pub fn get_supported_importers<T: InstalledBrowserRetriever>( // Check for installed browsers let installed_browsers = T::get_installed_browsers().unwrap_or_default(); - const IMPORTERS: [(&str, &str); 6] = [ + const IMPORTERS: &[(&str, &str)] = &[ ("chromecsv", "Chrome"), ("chromiumcsv", "Chromium"), ("bravecsv", "Brave"), @@ -57,9 +56,7 @@ pub fn get_supported_importers<T: InstalledBrowserRetriever>( map } -/* - Tests are cfg-gated based upon OS, and must be compiled/run on each OS for full coverage -*/ +// Tests are cfg-gated based upon OS, and must be compiled/run on each OS for full coverage #[cfg(test)] mod tests { use super::*; diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/util.rs b/apps/desktop/desktop_native/chromium_importer/src/util.rs similarity index 77% rename from apps/desktop/desktop_native/bitwarden_chromium_importer/src/util.rs rename to apps/desktop/desktop_native/chromium_importer/src/util.rs index e9c20ab621d..f346d7e6dd0 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/util.rs +++ b/apps/desktop/desktop_native/chromium_importer/src/util.rs @@ -1,9 +1,6 @@ -use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit}; use anyhow::{anyhow, Result}; -use pbkdf2::{hmac::Hmac, pbkdf2}; -use sha1::Sha1; -pub fn split_encrypted_string(encrypted: &[u8]) -> Result<(&str, &[u8])> { +fn split_encrypted_string(encrypted: &[u8]) -> Result<(&str, &[u8])> { if encrypted.len() < 3 { return Err(anyhow!( "Corrupted entry: invalid encrypted string length, expected at least 3 bytes, got {}", @@ -15,7 +12,14 @@ pub fn split_encrypted_string(encrypted: &[u8]) -> Result<(&str, &[u8])> { Ok((std::str::from_utf8(version)?, password)) } -pub fn split_encrypted_string_and_validate<'a>( +/// A Chromium password consists of three parts: +/// - Version (3 bytes): "v10", "v11", etc. +/// - Cipher text (chunks of 16 bytes) +/// - Padding (1-15 bytes) +/// +/// This function splits the encrypted byte slice into version and cipher text. +/// Padding is included and handled by the underlying cryptographic library. +pub(crate) fn split_encrypted_string_and_validate<'a>( encrypted: &'a [u8], supported_versions: &[&str], ) -> Result<(&'a str, &'a [u8])> { @@ -27,15 +31,22 @@ pub fn split_encrypted_string_and_validate<'a>( Ok((version, password)) } -pub fn decrypt_aes_128_cbc(key: &[u8], iv: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>> { - let decryptor = cbc::Decryptor::<aes::Aes128>::new_from_slices(key, iv)?; - let plaintext: Vec<u8> = decryptor +/// Decrypt using AES-128 in CBC mode. +#[cfg(any(target_os = "linux", target_os = "macos", test))] +pub(crate) fn decrypt_aes_128_cbc(key: &[u8], iv: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>> { + use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit}; + + cbc::Decryptor::<aes::Aes128>::new_from_slices(key, iv)? .decrypt_padded_vec_mut::<Pkcs7>(ciphertext) - .map_err(|e| anyhow!("Failed to decrypt: {}", e))?; - Ok(plaintext) + .map_err(|e| anyhow!("Failed to decrypt: {}", e)) } -pub fn derive_saltysalt(password: &[u8], iterations: u32) -> Result<Vec<u8>> { +/// Derives a PBKDF2 key from the static "saltysalt" salt with the given password and iteration count. +#[cfg(any(target_os = "linux", target_os = "macos"))] +pub(crate) fn derive_saltysalt(password: &[u8], iterations: u32) -> Result<Vec<u8>> { + use pbkdf2::{hmac::Hmac, pbkdf2}; + use sha1::Sha1; + let mut key = vec![0u8; 16]; pbkdf2::<Hmac<Sha1>>(password, b"saltysalt", iterations, &mut key) .map_err(|e| anyhow!("Failed to derive master key: {}", e))?; @@ -44,16 +55,6 @@ pub fn derive_saltysalt(password: &[u8], iterations: u32) -> Result<Vec<u8>> { #[cfg(test)] mod tests { - pub fn generate_vec(length: usize, offset: u8, increment: u8) -> Vec<u8> { - (0..length).map(|i| offset + i as u8 * increment).collect() - } - pub fn generate_generic_array<N: ArrayLength<u8>>( - offset: u8, - increment: u8, - ) -> GenericArray<u8, N> { - GenericArray::generate(|i| offset + i as u8 * increment) - } - use aes::cipher::{ block_padding::Pkcs7, generic_array::{sequence::GenericSequence, GenericArray}, @@ -64,6 +65,17 @@ mod tests { const LENGTH10: usize = 10; const LENGTH0: usize = 0; + fn generate_vec(length: usize, offset: u8, increment: u8) -> Vec<u8> { + (0..length).map(|i| offset + i as u8 * increment).collect() + } + + fn generate_generic_array<N: ArrayLength<u8>>( + offset: u8, + increment: u8, + ) -> GenericArray<u8, N> { + GenericArray::generate(|i| offset + i as u8 * increment) + } + fn run_split_encrypted_string_test<'a, const N: usize>( successfully_split: bool, plaintext_to_encrypt: &'a str, diff --git a/apps/desktop/desktop_native/napi/Cargo.toml b/apps/desktop/desktop_native/napi/Cargo.toml index 5e2e42b463f..4198baa4b5a 100644 --- a/apps/desktop/desktop_native/napi/Cargo.toml +++ b/apps/desktop/desktop_native/napi/Cargo.toml @@ -17,7 +17,7 @@ manual_test = [] anyhow = { workspace = true } autotype = { path = "../autotype" } base64 = { workspace = true } -bitwarden_chromium_importer = { path = "../bitwarden_chromium_importer" } +chromium_importer = { path = "../chromium_importer" } desktop_core = { path = "../core" } hex = { workspace = true } napi = { workspace = true, features = ["async"] } diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index 59751cd3246..cd49e5ac27a 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -3,15 +3,6 @@ /* auto-generated by NAPI-RS */ -/** Mechanisms that load data into the importer */ -export interface NativeImporterMetadata { - /** Identifies the importer */ - id: string - /** Describes the strategies used to obtain imported data */ - loaders: Array<string> - /** Identifies the instructions for the importer */ - instructions: string -} export declare namespace passwords { /** The error message returned when a password is not found during retrieval or deletion. */ export const PASSWORD_NOT_FOUND: string @@ -249,6 +240,11 @@ export declare namespace chromium_importer { login?: Login failure?: LoginImportFailure } + export interface NativeImporterMetadata { + id: string + loaders: Array<string> + instructions: string + } /** Returns OS aware metadata describing supported Chromium based importers as a JSON string. */ export function getMetadata(): Record<string, NativeImporterMetadata> export function getInstalledBrowsers(): Array<string> diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index 09f63f7854b..61453994d72 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -1064,11 +1064,13 @@ pub mod logging { #[napi] pub mod chromium_importer { - use bitwarden_chromium_importer::chromium::DefaultInstalledBrowserRetriever; - use bitwarden_chromium_importer::chromium::InstalledBrowserRetriever; - use bitwarden_chromium_importer::chromium::LoginImportResult as _LoginImportResult; - use bitwarden_chromium_importer::chromium::ProfileInfo as _ProfileInfo; - use bitwarden_chromium_importer::metadata::NativeImporterMetadata; + use chromium_importer::{ + chromium::{ + DefaultInstalledBrowserRetriever, InstalledBrowserRetriever, + LoginImportResult as _LoginImportResult, ProfileInfo as _ProfileInfo, + }, + metadata::NativeImporterMetadata as _NativeImporterMetadata, + }; use std::collections::HashMap; #[napi(object)] @@ -1098,6 +1100,13 @@ pub mod chromium_importer { pub failure: Option<LoginImportFailure>, } + #[napi(object)] + pub struct NativeImporterMetadata { + pub id: String, + pub loaders: Vec<&'static str>, + pub instructions: &'static str, + } + impl From<_LoginImportResult> for LoginImportResult { fn from(l: _LoginImportResult) -> Self { match l { @@ -1131,23 +1140,34 @@ pub mod chromium_importer { } } + impl From<_NativeImporterMetadata> for NativeImporterMetadata { + fn from(m: _NativeImporterMetadata) -> Self { + NativeImporterMetadata { + id: m.id, + loaders: m.loaders, + instructions: m.instructions, + } + } + } + #[napi] /// Returns OS aware metadata describing supported Chromium based importers as a JSON string. pub fn get_metadata() -> HashMap<String, NativeImporterMetadata> { - bitwarden_chromium_importer::metadata::get_supported_importers::< - DefaultInstalledBrowserRetriever, - >() + chromium_importer::metadata::get_supported_importers::<DefaultInstalledBrowserRetriever>() + .into_iter() + .map(|(browser, metadata)| (browser, NativeImporterMetadata::from(metadata))) + .collect() } #[napi] pub fn get_installed_browsers() -> napi::Result<Vec<String>> { - bitwarden_chromium_importer::chromium::DefaultInstalledBrowserRetriever::get_installed_browsers() + chromium_importer::chromium::DefaultInstalledBrowserRetriever::get_installed_browsers() .map_err(|e| napi::Error::from_reason(e.to_string())) } #[napi] pub fn get_available_profiles(browser: String) -> napi::Result<Vec<ProfileInfo>> { - bitwarden_chromium_importer::chromium::get_available_profiles(&browser) + chromium_importer::chromium::get_available_profiles(&browser) .map(|profiles| profiles.into_iter().map(ProfileInfo::from).collect()) .map_err(|e| napi::Error::from_reason(e.to_string())) } @@ -1157,7 +1177,7 @@ pub mod chromium_importer { browser: String, profile_id: String, ) -> napi::Result<Vec<LoginImportResult>> { - bitwarden_chromium_importer::chromium::import_logins(&browser, &profile_id) + chromium_importer::chromium::import_logins(&browser, &profile_id) .await .map(|logins| logins.into_iter().map(LoginImportResult::from).collect()) .map_err(|e| napi::Error::from_reason(e.to_string())) diff --git a/apps/desktop/src/app/tools/import/desktop-import-metadata.service.ts b/apps/desktop/src/app/tools/import/desktop-import-metadata.service.ts index fc2c2ff1183..0c29cd9f44a 100644 --- a/apps/desktop/src/app/tools/import/desktop-import-metadata.service.ts +++ b/apps/desktop/src/app/tools/import/desktop-import-metadata.service.ts @@ -1,5 +1,5 @@ import { SystemServiceProvider } from "@bitwarden/common/tools/providers"; -import type { NativeImporterMetadata } from "@bitwarden/desktop-napi"; +import type { chromium_importer } from "@bitwarden/desktop-napi"; import { ImportType, DefaultImportMetadataService, @@ -25,7 +25,9 @@ export class DesktopImportMetadataService await super.init(); } - private async parseNativeMetaData(raw: Record<string, NativeImporterMetadata>): Promise<void> { + private async parseNativeMetaData( + raw: Record<string, chromium_importer.NativeImporterMetadata>, + ): Promise<void> { const entries = Object.entries(raw).map(([id, meta]) => { const loaders = meta.loaders.map(this.mapLoader); const instructions = this.mapInstructions(meta.instructions); diff --git a/apps/desktop/src/app/tools/preload.ts b/apps/desktop/src/app/tools/preload.ts index 4d629c992ad..b872f108551 100644 --- a/apps/desktop/src/app/tools/preload.ts +++ b/apps/desktop/src/app/tools/preload.ts @@ -1,9 +1,9 @@ import { ipcRenderer } from "electron"; -import type { NativeImporterMetadata } from "@bitwarden/desktop-napi"; +import type { chromium_importer } from "@bitwarden/desktop-napi"; const chromiumImporter = { - getMetadata: (): Promise<Record<string, NativeImporterMetadata>> => + getMetadata: (): Promise<Record<string, chromium_importer.NativeImporterMetadata>> => ipcRenderer.invoke("chromium_importer.getMetadata"), getInstalledBrowsers: (): Promise<string[]> => ipcRenderer.invoke("chromium_importer.getInstalledBrowsers"), From 2ff9c23dc4d96b5c038e8468e37a4791a0aca9a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauritz=20Sj=C3=B6din?= <67279312+Mauritz8@users.noreply.github.com> Date: Mon, 27 Oct 2025 17:34:22 +0100 Subject: [PATCH 54/73] fix: prevent action buttons from overflowing for long passwords (#17027) Co-authored-by: Bryan Cunningham <bcunningham@bitwarden.com> --- libs/components/src/form-field/form-field.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/components/src/form-field/form-field.component.html b/libs/components/src/form-field/form-field.component.html index c2c92104727..a4af25a2492 100644 --- a/libs/components/src/form-field/form-field.component.html +++ b/libs/components/src/form-field/form-field.component.html @@ -97,7 +97,7 @@ <ng-container *ngTemplateOutlet="prefixContent"></ng-container> </div> <div - class="tw-w-full tw-pb-0 tw-relative [&>*]:tw-p-0 [&>*::selection]:tw-bg-primary-700 [&>*::selection]:tw-text-contrast" + class="tw-w-full tw-min-w-0 tw-pb-0 tw-relative [&>*]:tw-p-0 [&>*::selection]:tw-bg-primary-700 [&>*::selection]:tw-text-contrast" data-default-content > <ng-container *ngTemplateOutlet="defaultContent"></ng-container> From 32a40a91f50fcb0d26e72c838c6bedbb2aff6dc4 Mon Sep 17 00:00:00 2001 From: Alex <55413326+AlexRubik@users.noreply.github.com> Date: Mon, 27 Oct 2025 14:38:49 -0400 Subject: [PATCH 55/73] [PM-26929] filter at risk passwords count to only critical apps (#16879) * filter at risk passwords count to only critical apps * PM-26929 assign tasks to those apps that are marked as critical --------- Co-authored-by: voommen-livefront <voommen@livefront.com> --- .../services/view/all-activities.service.ts | 9 +++------ .../activity-cards/password-change-metric.component.ts | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/view/all-activities.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/view/all-activities.service.ts index 97db491823c..c275ad8c355 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/view/all-activities.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/view/all-activities.service.ts @@ -76,12 +76,9 @@ export class AllActivitiesService { } setAllAppsReportDetails(applications: ApplicationHealthReportDetailEnriched[]) { - // Only count at-risk passwords for CRITICAL applications - const criticalApps = applications.filter((app) => app.isMarkedAsCritical); - const totalAtRiskPasswords = criticalApps.reduce( - (sum, app) => sum + app.atRiskPasswordCount, - 0, - ); + const totalAtRiskPasswords = applications + .filter((app) => app.isMarkedAsCritical) + .reduce((sum, app) => sum + app.atRiskPasswordCount, 0); this.atRiskPasswordsCountSubject$.next(totalAtRiskPasswords); this.allApplicationsDetailsSubject$.next(applications); diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-cards/password-change-metric.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-cards/password-change-metric.component.ts index 941d693940b..5c03534720e 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-cards/password-change-metric.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-cards/password-change-metric.component.ts @@ -191,7 +191,7 @@ export class PasswordChangeMetricComponent implements OnInit { async assignTasks() { await this.accessIntelligenceSecurityTasksService.assignTasks( this.organizationId, - this.allApplicationsDetails, + this.allApplicationsDetails.filter((app) => app.isMarkedAsCritical), ); } } From 47975fda37d5e68109de9485fe073f67a1bfe3c2 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Mon, 27 Oct 2025 20:24:36 +0100 Subject: [PATCH 56/73] Address issues with eslint rules regarding signals and OnPush change detection (#17057) Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com> --- .../auto-confirm-edit-policy-dialog.component.ts | 16 ++++++++++------ .../auto-confirm-policy.component.ts | 6 ++++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/policies/auto-confirm-edit-policy-dialog.component.ts b/apps/web/src/app/admin-console/organizations/policies/auto-confirm-edit-policy-dialog.component.ts index 18a9306b7d1..55894aafd53 100644 --- a/apps/web/src/app/admin-console/organizations/policies/auto-confirm-edit-policy-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/auto-confirm-edit-policy-dialog.component.ts @@ -63,6 +63,8 @@ export type AutoConfirmPolicyDialogData = PolicyEditDialogData & { * Satisfies the PolicyDialogComponent interface structurally * via its static open() function. */ +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "auto-confirm-edit-policy-dialog.component.html", imports: [SharedModule], @@ -73,8 +75,8 @@ export class AutoConfirmPolicyDialogComponent { policyType = PolicyType; - protected firstTimeDialog = signal(false); - protected currentStep = signal(0); + protected readonly firstTimeDialog = signal(false); + protected readonly currentStep = signal(0); protected multiStepSubmit: Observable<MultiStepSubmit[]> = of([]); protected autoConfirmEnabled$: Observable<boolean> = this.accountService.activeAccount$.pipe( getUserId, @@ -82,11 +84,13 @@ export class AutoConfirmPolicyDialogComponent map((policies) => policies.find((p) => p.type === PolicyType.AutoConfirm)?.enabled ?? false), ); - private submitPolicy: Signal<TemplateRef<unknown> | undefined> = viewChild("step0"); - private openExtension: Signal<TemplateRef<unknown> | undefined> = viewChild("step1"); + private readonly submitPolicy: Signal<TemplateRef<unknown> | undefined> = viewChild("step0"); + private readonly openExtension: Signal<TemplateRef<unknown> | undefined> = viewChild("step1"); - private submitPolicyTitle: Signal<TemplateRef<unknown> | undefined> = viewChild("step0Title"); - private openExtensionTitle: Signal<TemplateRef<unknown> | undefined> = viewChild("step1Title"); + private readonly submitPolicyTitle: Signal<TemplateRef<unknown> | undefined> = + viewChild("step0Title"); + private readonly openExtensionTitle: Signal<TemplateRef<unknown> | undefined> = + viewChild("step1Title"); override policyComponent: AutoConfirmPolicyEditComponent | undefined; diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/auto-confirm-policy.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/auto-confirm-policy.component.ts index a5ea2ef8790..7fa4fc2eea7 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/auto-confirm-policy.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/auto-confirm-policy.component.ts @@ -26,14 +26,16 @@ export class AutoConfirmPolicy extends BasePolicyEditDefinition { } } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "auto-confirm-policy.component.html", imports: [SharedModule], }) export class AutoConfirmPolicyEditComponent extends BasePolicyEditComponent implements OnInit { protected readonly autoConfirmSvg = AutoConfirmSvg; - private policyForm: Signal<TemplateRef<any> | undefined> = viewChild("step0"); - private extensionButton: Signal<TemplateRef<any> | undefined> = viewChild("step1"); + private readonly policyForm: Signal<TemplateRef<any> | undefined> = viewChild("step0"); + private readonly extensionButton: Signal<TemplateRef<any> | undefined> = viewChild("step1"); protected step: number = 0; protected steps = [this.policyForm, this.extensionButton]; From 2b2b1f4a2744e497960d0445eea155438705721d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 17:29:45 -0400 Subject: [PATCH 57/73] [deps] Platform: Update @types/node to v22.18.11 (#15698) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../native-messaging-test-runner/package-lock.json | 8 ++++---- apps/desktop/native-messaging-test-runner/package.json | 2 +- package-lock.json | 8 ++++---- package.json | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/desktop/native-messaging-test-runner/package-lock.json b/apps/desktop/native-messaging-test-runner/package-lock.json index 3b976891014..b6e402a3ef6 100644 --- a/apps/desktop/native-messaging-test-runner/package-lock.json +++ b/apps/desktop/native-messaging-test-runner/package-lock.json @@ -19,7 +19,7 @@ "yargs": "18.0.0" }, "devDependencies": { - "@types/node": "22.15.3", + "@types/node": "22.18.11", "typescript": "5.4.2" } }, @@ -117,9 +117,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.15.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.3.tgz", - "integrity": "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==", + "version": "22.18.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.11.tgz", + "integrity": "sha512-Gd33J2XIrXurb+eT2ktze3rJAfAp9ZNjlBdh4SVgyrKEOADwCbdUDaK7QgJno8Ue4kcajscsKqu6n8OBG3hhCQ==", "license": "MIT", "peer": true, "dependencies": { diff --git a/apps/desktop/native-messaging-test-runner/package.json b/apps/desktop/native-messaging-test-runner/package.json index 0ca9cdc3a17..285997f6482 100644 --- a/apps/desktop/native-messaging-test-runner/package.json +++ b/apps/desktop/native-messaging-test-runner/package.json @@ -24,7 +24,7 @@ "yargs": "18.0.0" }, "devDependencies": { - "@types/node": "22.15.3", + "@types/node": "22.18.11", "typescript": "5.4.2" }, "_moduleAliases": { diff --git a/package-lock.json b/package-lock.json index 747576d4ca2..e131618ee4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -112,7 +112,7 @@ "@types/koa-json": "2.0.23", "@types/lowdb": "1.0.15", "@types/lunr": "2.3.7", - "@types/node": "22.15.3", + "@types/node": "22.18.11", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", "@types/papaparse": "5.3.16", @@ -14391,9 +14391,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.15.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.3.tgz", - "integrity": "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==", + "version": "22.18.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.11.tgz", + "integrity": "sha512-Gd33J2XIrXurb+eT2ktze3rJAfAp9ZNjlBdh4SVgyrKEOADwCbdUDaK7QgJno8Ue4kcajscsKqu6n8OBG3hhCQ==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" diff --git a/package.json b/package.json index c241e07e2e1..2c02ff68824 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "@types/koa-json": "2.0.23", "@types/lowdb": "1.0.15", "@types/lunr": "2.3.7", - "@types/node": "22.15.3", + "@types/node": "22.18.11", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", "@types/papaparse": "5.3.16", From 8eef78960d0334ac6dd2dd12cfff9007d405c977 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Tue, 28 Oct 2025 11:13:58 +0100 Subject: [PATCH 58/73] [PM-27358] Remove unused getInstalledBrowsers method (#17019) * Remove unused getInstalledBrowsers metthod * Run cargo fmt --------- Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com> --- apps/desktop/desktop_native/napi/index.d.ts | 1 - apps/desktop/desktop_native/napi/src/lib.rs | 10 ++-------- .../src/app/tools/import/chromium-importer.service.ts | 4 ---- apps/desktop/src/app/tools/preload.ts | 2 -- 4 files changed, 2 insertions(+), 15 deletions(-) diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index cd49e5ac27a..0a8beb8c427 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -247,7 +247,6 @@ export declare namespace chromium_importer { } /** Returns OS aware metadata describing supported Chromium based importers as a JSON string. */ export function getMetadata(): Record<string, NativeImporterMetadata> - export function getInstalledBrowsers(): Array<string> export function getAvailableProfiles(browser: string): Array<ProfileInfo> export function importLogins(browser: string, profileId: string): Promise<Array<LoginImportResult>> } diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index 61453994d72..39e57bd0bb5 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -1066,8 +1066,8 @@ pub mod logging { pub mod chromium_importer { use chromium_importer::{ chromium::{ - DefaultInstalledBrowserRetriever, InstalledBrowserRetriever, - LoginImportResult as _LoginImportResult, ProfileInfo as _ProfileInfo, + DefaultInstalledBrowserRetriever, LoginImportResult as _LoginImportResult, + ProfileInfo as _ProfileInfo, }, metadata::NativeImporterMetadata as _NativeImporterMetadata, }; @@ -1159,12 +1159,6 @@ pub mod chromium_importer { .collect() } - #[napi] - pub fn get_installed_browsers() -> napi::Result<Vec<String>> { - chromium_importer::chromium::DefaultInstalledBrowserRetriever::get_installed_browsers() - .map_err(|e| napi::Error::from_reason(e.to_string())) - } - #[napi] pub fn get_available_profiles(browser: String) -> napi::Result<Vec<ProfileInfo>> { chromium_importer::chromium::get_available_profiles(&browser) diff --git a/apps/desktop/src/app/tools/import/chromium-importer.service.ts b/apps/desktop/src/app/tools/import/chromium-importer.service.ts index 5273eef4b54..0faff81974a 100644 --- a/apps/desktop/src/app/tools/import/chromium-importer.service.ts +++ b/apps/desktop/src/app/tools/import/chromium-importer.service.ts @@ -8,10 +8,6 @@ export class ChromiumImporterService { return await chromium_importer.getMetadata(); }); - ipcMain.handle("chromium_importer.getInstalledBrowsers", async (event) => { - return await chromium_importer.getInstalledBrowsers(); - }); - ipcMain.handle("chromium_importer.getAvailableProfiles", async (event, browser: string) => { return await chromium_importer.getAvailableProfiles(browser); }); diff --git a/apps/desktop/src/app/tools/preload.ts b/apps/desktop/src/app/tools/preload.ts index b872f108551..c21a1ac0bfc 100644 --- a/apps/desktop/src/app/tools/preload.ts +++ b/apps/desktop/src/app/tools/preload.ts @@ -5,8 +5,6 @@ import type { chromium_importer } from "@bitwarden/desktop-napi"; const chromiumImporter = { getMetadata: (): Promise<Record<string, chromium_importer.NativeImporterMetadata>> => ipcRenderer.invoke("chromium_importer.getMetadata"), - getInstalledBrowsers: (): Promise<string[]> => - ipcRenderer.invoke("chromium_importer.getInstalledBrowsers"), getAvailableProfiles: (browser: string): Promise<any[]> => ipcRenderer.invoke("chromium_importer.getAvailableProfiles", browser), importLogins: (browser: string, profileId: string): Promise<any[]> => From af061282c6679c4eb24ffd5dc1e2cc41cebd7927 Mon Sep 17 00:00:00 2001 From: Jonathan Prusik <jprusik@users.noreply.github.com> Date: Tue, 28 Oct 2025 09:36:33 -0400 Subject: [PATCH 59/73] =?UTF-8?q?do=20not=20multiply=20delay=20of=20fill?= =?UTF-8?q?=20script=20action=20execution=20=F0=9F=95=B4=EF=B8=8F=20(#1704?= =?UTF-8?q?9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Miles Blackwood <milesblackwoodmusic@gmail.com> --- .../src/autofill/services/insert-autofill-content.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/browser/src/autofill/services/insert-autofill-content.service.ts b/apps/browser/src/autofill/services/insert-autofill-content.service.ts index 9ddbcdc005d..a809dadf8ed 100644 --- a/apps/browser/src/autofill/services/insert-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/insert-autofill-content.service.ts @@ -136,7 +136,7 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf setTimeout(() => { this.autofillInsertActions[action]({ opid, value }); resolve(); - }, delayActionsInMilliseconds * actionIndex), + }, delayActionsInMilliseconds), ); }; From 8162c06700e7573e99c09670beb142cba48e18df Mon Sep 17 00:00:00 2001 From: Brandon Treston <btreston@bitwarden.com> Date: Tue, 28 Oct 2025 09:47:54 -0400 Subject: [PATCH 60/73] [PM-26372] Add auto confirm service (#17001) * add state definition for auto confirm * typo * refactor organziation user service * WIP create auto confirm service * add POST method, finish implementation * add missing userId param, jsdoc * fix DI * refactor organziation user service * WIP create auto confirm service * add POST method, finish implementation * add missing userId param, jsdoc * clean up, more DI fixes * remove @Injectable from service, fix tests * remove from libs/common, fix dir structure, add tests --- .../bulk/bulk-confirm-dialog.component.ts | 5 +- .../organizations/members/services/index.ts | 1 - .../member-actions.service.spec.ts | 10 +- .../member-actions/member-actions.service.ts | 6 +- .../auto-confirm.service.abstraction.ts | 42 ++ .../common/auto-confirm/abstractions/index.ts | 1 + .../src/common/auto-confirm/index.ts | 3 + .../models/auto-confirm-state.model.ts} | 2 +- .../src/common/auto-confirm/models/index.ts | 1 + .../default-auto-confirm.service.spec.ts | 382 ++++++++++++++++++ .../services/default-auto-confirm.service.ts | 90 +++++ .../src/common/auto-confirm/services/index.ts | 1 + libs/admin-console/src/common/index.ts | 3 +- .../organization-user/abstractions/index.ts | 1 + .../organization-user-api.service.ts | 13 + .../abstractions/organization-user.service.ts | 45 +++ .../default-organization-user-api.service.ts | 14 + .../default-organization-user.service.spec.ts | 26 +- .../default-organization-user.service.ts | 36 +- .../organization-user/services/index.ts | 1 + 20 files changed, 638 insertions(+), 45 deletions(-) create mode 100644 libs/admin-console/src/common/auto-confirm/abstractions/auto-confirm.service.abstraction.ts create mode 100644 libs/admin-console/src/common/auto-confirm/abstractions/index.ts create mode 100644 libs/admin-console/src/common/auto-confirm/index.ts rename libs/{common/src/admin-console/services/auto-confirm/auto-confirm.state.ts => admin-console/src/common/auto-confirm/models/auto-confirm-state.model.ts} (84%) create mode 100644 libs/admin-console/src/common/auto-confirm/models/index.ts create mode 100644 libs/admin-console/src/common/auto-confirm/services/default-auto-confirm.service.spec.ts create mode 100644 libs/admin-console/src/common/auto-confirm/services/default-auto-confirm.service.ts create mode 100644 libs/admin-console/src/common/auto-confirm/services/index.ts create mode 100644 libs/admin-console/src/common/organization-user/abstractions/organization-user.service.ts rename apps/web/src/app/admin-console/organizations/members/services/organization-user/organization-user.service.spec.ts => libs/admin-console/src/common/organization-user/services/default-organization-user.service.spec.ts (91%) rename apps/web/src/app/admin-console/organizations/members/services/organization-user/organization-user.service.ts => libs/admin-console/src/common/organization-user/services/default-organization-user.service.ts (80%) diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.ts index 55385ca0ce9..81930279184 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.ts @@ -5,6 +5,7 @@ import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { firstValueFrom, map, Observable, switchMap } from "rxjs"; import { + DefaultOrganizationUserService, OrganizationUserApiService, OrganizationUserBulkConfirmRequest, OrganizationUserBulkPublicKeyResponse, @@ -26,8 +27,6 @@ import { OrgKey } from "@bitwarden/common/types/key"; import { DIALOG_DATA, DialogConfig, DialogService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; -import { OrganizationUserService } from "../../services/organization-user/organization-user.service"; - import { BaseBulkConfirmComponent } from "./base-bulk-confirm.component"; import { BulkUserDetails } from "./bulk-status.component"; @@ -54,7 +53,7 @@ export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent { private organizationUserApiService: OrganizationUserApiService, protected i18nService: I18nService, private stateProvider: StateProvider, - private organizationUserService: OrganizationUserService, + private organizationUserService: DefaultOrganizationUserService, private configService: ConfigService, ) { super(keyService, encryptService, i18nService); diff --git a/apps/web/src/app/admin-console/organizations/members/services/index.ts b/apps/web/src/app/admin-console/organizations/members/services/index.ts index 2ac2d31cd69..baaa33eeae9 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/index.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/index.ts @@ -2,4 +2,3 @@ export { OrganizationMembersService } from "./organization-members-service/organ export { MemberActionsService } from "./member-actions/member-actions.service"; export { MemberDialogManagerService } from "./member-dialog-manager/member-dialog-manager.service"; export { DeleteManagedMemberWarningService } from "./delete-managed-member/delete-managed-member-warning.service"; -export { OrganizationUserService } from "./organization-user/organization-user.service"; diff --git a/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.spec.ts b/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.spec.ts index 6fd7de7b292..e856ab7afd1 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.spec.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.spec.ts @@ -10,6 +10,7 @@ import { OrganizationUserStatusType, } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { OrganizationMetadataServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-metadata.service.abstraction"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; @@ -20,7 +21,6 @@ import { OrgKey } from "@bitwarden/common/types/key"; import { newGuid } from "@bitwarden/guid"; import { KeyService } from "@bitwarden/key-management"; -import { BillingConstraintService } from "../../../../../billing/members/billing-constraint/billing-constraint.service"; import { OrganizationUserView } from "../../../core/views/organization-user.view"; import { OrganizationUserService } from "../organization-user/organization-user.service"; @@ -34,7 +34,7 @@ describe("MemberActionsService", () => { let encryptService: MockProxy<EncryptService>; let configService: MockProxy<ConfigService>; let accountService: FakeAccountService; - let billingConstraintService: MockProxy<BillingConstraintService>; + let organizationMetadataService: MockProxy<OrganizationMetadataServiceAbstraction>; const userId = newGuid() as UserId; const organizationId = newGuid() as OrganizationId; @@ -50,7 +50,7 @@ describe("MemberActionsService", () => { encryptService = mock<EncryptService>(); configService = mock<ConfigService>(); accountService = mockAccountServiceWith(userId); - billingConstraintService = mock<BillingConstraintService>(); + organizationMetadataService = mock<OrganizationMetadataServiceAbstraction>(); mockOrganization = { id: organizationId, @@ -75,7 +75,7 @@ describe("MemberActionsService", () => { encryptService, configService, accountService, - billingConstraintService, + organizationMetadataService, ); }); @@ -251,7 +251,7 @@ describe("MemberActionsService", () => { expect(result).toEqual({ success: true }); expect(organizationUserService.confirmUser).toHaveBeenCalledWith( mockOrganization, - mockOrgUser, + mockOrgUser.id, publicKey, ); expect(organizationUserApiService.postOrganizationUserConfirm).not.toHaveBeenCalled(); diff --git a/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.ts b/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.ts index 3697aba94ff..2913e90e6c0 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.ts @@ -2,6 +2,7 @@ import { Injectable } from "@angular/core"; import { firstValueFrom, switchMap, map } from "rxjs"; import { + DefaultOrganizationUserService, OrganizationUserApiService, OrganizationUserBulkResponse, OrganizationUserConfirmRequest, @@ -21,7 +22,6 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co import { KeyService } from "@bitwarden/key-management"; import { OrganizationUserView } from "../../../core/views/organization-user.view"; -import { OrganizationUserService } from "../organization-user/organization-user.service"; export interface MemberActionResult { success: boolean; @@ -39,7 +39,7 @@ export class MemberActionsService { constructor( private organizationUserApiService: OrganizationUserApiService, - private organizationUserService: OrganizationUserService, + private organizationUserService: DefaultOrganizationUserService, private keyService: KeyService, private encryptService: EncryptService, private configService: ConfigService, @@ -129,7 +129,7 @@ export class MemberActionsService { await firstValueFrom(this.configService.getFeatureFlag$(FeatureFlag.CreateDefaultLocation)) ) { await firstValueFrom( - this.organizationUserService.confirmUser(organization, user, publicKey), + this.organizationUserService.confirmUser(organization, user.id, publicKey), ); } else { const request = await firstValueFrom( diff --git a/libs/admin-console/src/common/auto-confirm/abstractions/auto-confirm.service.abstraction.ts b/libs/admin-console/src/common/auto-confirm/abstractions/auto-confirm.service.abstraction.ts new file mode 100644 index 00000000000..e753184273e --- /dev/null +++ b/libs/admin-console/src/common/auto-confirm/abstractions/auto-confirm.service.abstraction.ts @@ -0,0 +1,42 @@ +import { Observable } from "rxjs"; + +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { OrganizationId } from "@bitwarden/common/types/guid"; +import { UserId } from "@bitwarden/user-core"; + +import { AutoConfirmState } from "../models/auto-confirm-state.model"; + +export abstract class AutomaticUserConfirmationService { + /** + * @param userId + * @returns Observable<AutoConfirmState> an observable with the Auto Confirm user state for the provided userId. + **/ + abstract configuration$(userId: UserId): Observable<AutoConfirmState>; + /** + * Upserts the existing user state with a new configuration. + * @param userId + * @param config The new AutoConfirmState to upsert into the user state for the provided userId. + **/ + abstract upsert(userId: UserId, config: AutoConfirmState): Promise<void>; + /** + * This will check if the feature is enabled, the organization plan feature UseAutomaticUserConfirmation is enabled + * and the the provided user has admin/owner/manage custom permission role. + * @param userId + * @returns Observable<boolean> an observable with a boolean telling us if the provided user may confgure the auto confirm feature. + **/ + abstract canManageAutoConfirm$( + userId: UserId, + organizationId: OrganizationId, + ): Observable<boolean>; + /** + * Calls the API endpoint to initiate automatic user confirmation. + * @param userId The userId of the logged in admin performing auto confirmation. This is neccesary to perform the key exchange and for permissions checks. + * @param confirmingUserId The userId of the user being confirmed. + * @param organization the organization the user is being auto confirmed to. + **/ + abstract autoConfirmUser( + userId: UserId, + confirmingUserId: UserId, + organization: Organization, + ): Promise<void>; +} diff --git a/libs/admin-console/src/common/auto-confirm/abstractions/index.ts b/libs/admin-console/src/common/auto-confirm/abstractions/index.ts new file mode 100644 index 00000000000..87e284656ab --- /dev/null +++ b/libs/admin-console/src/common/auto-confirm/abstractions/index.ts @@ -0,0 +1 @@ +export * from "./auto-confirm.service.abstraction"; diff --git a/libs/admin-console/src/common/auto-confirm/index.ts b/libs/admin-console/src/common/auto-confirm/index.ts new file mode 100644 index 00000000000..9187ccd39cf --- /dev/null +++ b/libs/admin-console/src/common/auto-confirm/index.ts @@ -0,0 +1,3 @@ +export * from "./abstractions"; +export * from "./models"; +export * from "./services"; diff --git a/libs/common/src/admin-console/services/auto-confirm/auto-confirm.state.ts b/libs/admin-console/src/common/auto-confirm/models/auto-confirm-state.model.ts similarity index 84% rename from libs/common/src/admin-console/services/auto-confirm/auto-confirm.state.ts rename to libs/admin-console/src/common/auto-confirm/models/auto-confirm-state.model.ts index b97f980b644..c69db69746c 100644 --- a/libs/common/src/admin-console/services/auto-confirm/auto-confirm.state.ts +++ b/libs/admin-console/src/common/auto-confirm/models/auto-confirm-state.model.ts @@ -1,4 +1,4 @@ -import { AUTO_CONFIRM, UserKeyDefinition } from "../../../platform/state"; +import { AUTO_CONFIRM, UserKeyDefinition } from "@bitwarden/state"; export class AutoConfirmState { enabled: boolean; diff --git a/libs/admin-console/src/common/auto-confirm/models/index.ts b/libs/admin-console/src/common/auto-confirm/models/index.ts new file mode 100644 index 00000000000..a34c54c16aa --- /dev/null +++ b/libs/admin-console/src/common/auto-confirm/models/index.ts @@ -0,0 +1 @@ +export * from "./auto-confirm-state.model"; diff --git a/libs/admin-console/src/common/auto-confirm/services/default-auto-confirm.service.spec.ts b/libs/admin-console/src/common/auto-confirm/services/default-auto-confirm.service.spec.ts new file mode 100644 index 00000000000..133dac758b4 --- /dev/null +++ b/libs/admin-console/src/common/auto-confirm/services/default-auto-confirm.service.spec.ts @@ -0,0 +1,382 @@ +import { TestBed } from "@angular/core/testing"; +import { BehaviorSubject, firstValueFrom, of, throwError } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api"; +import { OrganizationData } from "@bitwarden/common/admin-console/models/data/organization.data"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeStateProvider, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; + +import { + DefaultOrganizationUserService, + OrganizationUserApiService, + OrganizationUserConfirmRequest, +} from "../../organization-user"; +import { AUTO_CONFIRM_STATE, AutoConfirmState } from "../models/auto-confirm-state.model"; + +import { DefaultAutomaticUserConfirmationService } from "./default-auto-confirm.service"; + +describe("DefaultAutomaticUserConfirmationService", () => { + let service: DefaultAutomaticUserConfirmationService; + let configService: jest.Mocked<ConfigService>; + let apiService: jest.Mocked<ApiService>; + let organizationUserService: jest.Mocked<DefaultOrganizationUserService>; + let stateProvider: FakeStateProvider; + let organizationService: jest.Mocked<InternalOrganizationServiceAbstraction>; + let organizationUserApiService: jest.Mocked<OrganizationUserApiService>; + + const mockUserId = Utils.newGuid() as UserId; + const mockConfirmingUserId = Utils.newGuid() as UserId; + const mockOrganizationId = Utils.newGuid() as OrganizationId; + let mockOrganization: Organization; + + beforeEach(() => { + configService = { + getFeatureFlag$: jest.fn(), + } as any; + + apiService = { + getUserPublicKey: jest.fn(), + } as any; + + organizationUserService = { + buildConfirmRequest: jest.fn(), + } as any; + + stateProvider = new FakeStateProvider(mockAccountServiceWith(mockUserId)); + + organizationService = { + organizations$: jest.fn(), + } as any; + + organizationUserApiService = { + postOrganizationUserConfirm: jest.fn(), + } as any; + + TestBed.configureTestingModule({ + providers: [ + DefaultAutomaticUserConfirmationService, + { provide: ConfigService, useValue: configService }, + { provide: ApiService, useValue: apiService }, + { provide: DefaultOrganizationUserService, useValue: organizationUserService }, + { provide: "StateProvider", useValue: stateProvider }, + { + provide: InternalOrganizationServiceAbstraction, + useValue: organizationService, + }, + { provide: OrganizationUserApiService, useValue: organizationUserApiService }, + ], + }); + + service = new DefaultAutomaticUserConfirmationService( + configService, + apiService, + organizationUserService, + stateProvider, + organizationService, + organizationUserApiService, + ); + + const mockOrgData = new OrganizationData({} as any, {} as any); + mockOrgData.id = mockOrganizationId; + mockOrgData.useAutomaticUserConfirmation = true; + + const permissions = new PermissionsApi(); + permissions.manageUsers = true; + mockOrgData.permissions = permissions; + + mockOrganization = new Organization(mockOrgData); + }); + + describe("configuration$", () => { + it("should return default AutoConfirmState when no state exists", async () => { + const config$ = service.configuration$(mockUserId); + const config = await firstValueFrom(config$); + + expect(config).toBeInstanceOf(AutoConfirmState); + expect(config.enabled).toBe(false); + expect(config.showSetupDialog).toBe(true); + }); + + it("should return stored AutoConfirmState when state exists", async () => { + const expectedConfig = new AutoConfirmState(); + expectedConfig.enabled = true; + expectedConfig.showSetupDialog = false; + expectedConfig.showBrowserNotification = true; + + await stateProvider.setUserState( + AUTO_CONFIRM_STATE, + { [mockUserId]: expectedConfig }, + mockUserId, + ); + + const config$ = service.configuration$(mockUserId); + const config = await firstValueFrom(config$); + + expect(config.enabled).toBe(true); + expect(config.showSetupDialog).toBe(false); + expect(config.showBrowserNotification).toBe(true); + }); + + it("should emit updates when state changes", async () => { + const config$ = service.configuration$(mockUserId); + const configs: AutoConfirmState[] = []; + + const subscription = config$.subscribe((config) => configs.push(config)); + + expect(configs[0].enabled).toBe(false); + + const newConfig = new AutoConfirmState(); + newConfig.enabled = true; + await stateProvider.setUserState(AUTO_CONFIRM_STATE, { [mockUserId]: newConfig }, mockUserId); + + expect(configs.length).toBeGreaterThan(1); + expect(configs[configs.length - 1].enabled).toBe(true); + + subscription.unsubscribe(); + }); + }); + + describe("upsert", () => { + it("should store new configuration for user", async () => { + const newConfig = new AutoConfirmState(); + newConfig.enabled = true; + newConfig.showSetupDialog = false; + + await service.upsert(mockUserId, newConfig); + + const storedState = await firstValueFrom( + stateProvider.getUser(mockUserId, AUTO_CONFIRM_STATE).state$, + ); + + expect(storedState != null); + expect(storedState![mockUserId]).toEqual(newConfig); + }); + + it("should update existing configuration for user", async () => { + const initialConfig = new AutoConfirmState(); + initialConfig.enabled = false; + + await service.upsert(mockUserId, initialConfig); + + const updatedConfig = new AutoConfirmState(); + updatedConfig.enabled = true; + updatedConfig.showSetupDialog = false; + + await service.upsert(mockUserId, updatedConfig); + + const storedState = await firstValueFrom( + stateProvider.getUser(mockUserId, AUTO_CONFIRM_STATE).state$, + ); + + expect(storedState != null); + expect(storedState![mockUserId].enabled).toBe(true); + expect(storedState![mockUserId].showSetupDialog).toBe(false); + }); + + it("should preserve other user configurations when updating", async () => { + const otherUserId = Utils.newGuid() as UserId; + const otherConfig = new AutoConfirmState(); + otherConfig.enabled = true; + + await stateProvider.setUserState( + AUTO_CONFIRM_STATE, + { [otherUserId]: otherConfig }, + mockUserId, + ); + + const newConfig = new AutoConfirmState(); + newConfig.enabled = false; + + await service.upsert(mockUserId, newConfig); + + const storedState = await firstValueFrom( + stateProvider.getUser(mockUserId, AUTO_CONFIRM_STATE).state$, + ); + + expect(storedState != null); + expect(storedState![mockUserId]).toEqual(newConfig); + expect(storedState![otherUserId]).toEqual(otherConfig); + }); + }); + + describe("canManageAutoConfirm$", () => { + beforeEach(() => { + const organizations$ = new BehaviorSubject<Organization[]>([mockOrganization]); + organizationService.organizations$.mockReturnValue(organizations$); + }); + + it("should return true when feature flag is enabled and organization allows management", async () => { + configService.getFeatureFlag$.mockReturnValue(of(true)); + + const canManage$ = service.canManageAutoConfirm$(mockUserId, mockOrganizationId); + const canManage = await firstValueFrom(canManage$); + + expect(canManage).toBe(true); + }); + + it("should return false when feature flag is disabled", async () => { + configService.getFeatureFlag$.mockReturnValue(of(false)); + + const canManage$ = service.canManageAutoConfirm$(mockUserId, mockOrganizationId); + const canManage = await firstValueFrom(canManage$); + + expect(canManage).toBe(false); + }); + + it("should return false when organization canManageUsers is false", async () => { + configService.getFeatureFlag$.mockReturnValue(of(true)); + + // Create organization without manageUsers permission + const mockOrgData = new OrganizationData({} as any, {} as any); + mockOrgData.id = mockOrganizationId; + mockOrgData.useAutomaticUserConfirmation = true; + const permissions = new PermissionsApi(); + permissions.manageUsers = false; + mockOrgData.permissions = permissions; + const orgWithoutManageUsers = new Organization(mockOrgData); + + const organizations$ = new BehaviorSubject<Organization[]>([orgWithoutManageUsers]); + organizationService.organizations$.mockReturnValue(organizations$); + + const canManage$ = service.canManageAutoConfirm$(mockUserId, mockOrganizationId); + const canManage = await firstValueFrom(canManage$); + + expect(canManage).toBe(false); + }); + + it("should return false when organization useAutomaticUserConfirmation is false", async () => { + configService.getFeatureFlag$.mockReturnValue(of(true)); + + // Create organization without useAutomaticUserConfirmation + const mockOrgData = new OrganizationData({} as any, {} as any); + mockOrgData.id = mockOrganizationId; + mockOrgData.useAutomaticUserConfirmation = false; + const permissions = new PermissionsApi(); + permissions.manageUsers = true; + mockOrgData.permissions = permissions; + const orgWithoutAutoConfirm = new Organization(mockOrgData); + + const organizations$ = new BehaviorSubject<Organization[]>([orgWithoutAutoConfirm]); + organizationService.organizations$.mockReturnValue(organizations$); + + const canManage$ = service.canManageAutoConfirm$(mockUserId, mockOrganizationId); + const canManage = await firstValueFrom(canManage$); + + expect(canManage).toBe(false); + }); + + it("should return false when organization is not found", async () => { + configService.getFeatureFlag$.mockReturnValue(of(true)); + + const organizations$ = new BehaviorSubject<Organization[]>([]); + organizationService.organizations$.mockReturnValue(organizations$); + + const canManage$ = service.canManageAutoConfirm$(mockUserId, mockOrganizationId); + const canManage = await firstValueFrom(canManage$); + + expect(canManage).toBe(false); + }); + + it("should use the correct feature flag", async () => { + configService.getFeatureFlag$.mockReturnValue(of(true)); + + const canManage$ = service.canManageAutoConfirm$(mockUserId, mockOrganizationId); + await firstValueFrom(canManage$); + + expect(configService.getFeatureFlag$).toHaveBeenCalledWith(FeatureFlag.AutoConfirm); + }); + }); + + describe("autoConfirmUser", () => { + const mockPublicKey = "mock-public-key-base64"; + const mockPublicKeyArray = new Uint8Array([1, 2, 3, 4]); + const mockConfirmRequest = { + key: "encrypted-key", + defaultUserCollectionName: "encrypted-collection", + } as OrganizationUserConfirmRequest; + + beforeEach(() => { + const organizations$ = new BehaviorSubject<Organization[]>([mockOrganization]); + organizationService.organizations$.mockReturnValue(organizations$); + configService.getFeatureFlag$.mockReturnValue(of(true)); + + apiService.getUserPublicKey.mockResolvedValue({ publicKey: mockPublicKey } as any); + jest.spyOn(Utils, "fromB64ToArray").mockReturnValue(mockPublicKeyArray); + organizationUserService.buildConfirmRequest.mockReturnValue(of(mockConfirmRequest)); + organizationUserApiService.postOrganizationUserConfirm.mockResolvedValue(undefined); + }); + + it("should successfully auto-confirm a user", async () => { + await service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganization); + + expect(apiService.getUserPublicKey).toHaveBeenCalledWith(mockUserId); + expect(organizationUserService.buildConfirmRequest).toHaveBeenCalledWith( + mockOrganization, + mockPublicKeyArray, + ); + expect(organizationUserApiService.postOrganizationUserConfirm).toHaveBeenCalledWith( + mockOrganizationId, + mockConfirmingUserId, + mockConfirmRequest, + ); + }); + + it("should not confirm user when canManageAutoConfirm returns false", async () => { + configService.getFeatureFlag$.mockReturnValue(of(false)); + + await expect( + service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganization), + ).rejects.toThrow("Cannot automatically confirm user (insufficient permissions)"); + + expect(apiService.getUserPublicKey).not.toHaveBeenCalled(); + expect(organizationUserApiService.postOrganizationUserConfirm).not.toHaveBeenCalled(); + }); + + it("should build confirm request with organization and public key", async () => { + await service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganization); + + expect(organizationUserService.buildConfirmRequest).toHaveBeenCalledWith( + mockOrganization, + mockPublicKeyArray, + ); + }); + + it("should call API with correct parameters", async () => { + await service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganization); + + expect(organizationUserApiService.postOrganizationUserConfirm).toHaveBeenCalledWith( + mockOrganization.id, + mockConfirmingUserId, + mockConfirmRequest, + ); + }); + + it("should handle API errors gracefully", async () => { + const apiError = new Error("API Error"); + apiService.getUserPublicKey.mockRejectedValue(apiError); + + await expect( + service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganization), + ).rejects.toThrow("API Error"); + + expect(organizationUserApiService.postOrganizationUserConfirm).not.toHaveBeenCalled(); + }); + + it("should handle buildConfirmRequest errors gracefully", async () => { + const buildError = new Error("Build Error"); + organizationUserService.buildConfirmRequest.mockReturnValue(throwError(() => buildError)); + + await expect( + service.autoConfirmUser(mockUserId, mockConfirmingUserId, mockOrganization), + ).rejects.toThrow("Build Error"); + + expect(organizationUserApiService.postOrganizationUserConfirm).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/libs/admin-console/src/common/auto-confirm/services/default-auto-confirm.service.ts b/libs/admin-console/src/common/auto-confirm/services/default-auto-confirm.service.ts new file mode 100644 index 00000000000..a906a2ddc4a --- /dev/null +++ b/libs/admin-console/src/common/auto-confirm/services/default-auto-confirm.service.ts @@ -0,0 +1,90 @@ +import { combineLatest, firstValueFrom, map, Observable, switchMap } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { getById } from "@bitwarden/common/platform/misc"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { OrganizationId } from "@bitwarden/common/types/guid"; +import { StateProvider } from "@bitwarden/state"; +import { UserId } from "@bitwarden/user-core"; + +import { + DefaultOrganizationUserService, + OrganizationUserApiService, +} from "../../organization-user"; +import { AutomaticUserConfirmationService } from "../abstractions/auto-confirm.service.abstraction"; +import { AUTO_CONFIRM_STATE, AutoConfirmState } from "../models/auto-confirm-state.model"; + +export class DefaultAutomaticUserConfirmationService implements AutomaticUserConfirmationService { + constructor( + private configService: ConfigService, + private apiService: ApiService, + private organizationUserService: DefaultOrganizationUserService, + private stateProvider: StateProvider, + private organizationService: InternalOrganizationServiceAbstraction, + private organizationUserApiService: OrganizationUserApiService, + ) {} + private autoConfirmState(userId: UserId) { + return this.stateProvider.getUser(userId, AUTO_CONFIRM_STATE); + } + + configuration$(userId: UserId): Observable<AutoConfirmState> { + return this.autoConfirmState(userId).state$.pipe( + map((records) => records?.[userId] ?? new AutoConfirmState()), + ); + } + + async upsert(userId: UserId, config: AutoConfirmState): Promise<void> { + await this.autoConfirmState(userId).update((records) => { + return { + ...records, + [userId]: config, + }; + }); + } + + canManageAutoConfirm$(userId: UserId, organizationId: OrganizationId): Observable<boolean> { + return combineLatest([ + this.configService.getFeatureFlag$(FeatureFlag.AutoConfirm), + this.organizationService.organizations$(userId).pipe(getById(organizationId)), + ]).pipe( + map( + ([enabled, organization]) => + (enabled && organization?.canManageUsers && organization?.useAutomaticUserConfirmation) ?? + false, + ), + ); + } + + async autoConfirmUser( + userId: UserId, + confirmingUserId: UserId, + organization: Organization, + ): Promise<void> { + await firstValueFrom( + this.canManageAutoConfirm$(userId, organization.id).pipe( + map((canManage) => { + if (!canManage) { + throw new Error("Cannot automatically confirm user (insufficient permissions)"); + } + return canManage; + }), + switchMap(() => this.apiService.getUserPublicKey(userId)), + map((publicKeyResponse) => Utils.fromB64ToArray(publicKeyResponse.publicKey)), + switchMap((publicKey) => + this.organizationUserService.buildConfirmRequest(organization, publicKey), + ), + switchMap((request) => + this.organizationUserApiService.postOrganizationUserConfirm( + organization.id, + confirmingUserId, + request, + ), + ), + ), + ); + } +} diff --git a/libs/admin-console/src/common/auto-confirm/services/index.ts b/libs/admin-console/src/common/auto-confirm/services/index.ts new file mode 100644 index 00000000000..305ae380848 --- /dev/null +++ b/libs/admin-console/src/common/auto-confirm/services/index.ts @@ -0,0 +1 @@ +export * from "./default-auto-confirm.service"; diff --git a/libs/admin-console/src/common/index.ts b/libs/admin-console/src/common/index.ts index edeff5aa314..37f79d56256 100644 --- a/libs/admin-console/src/common/index.ts +++ b/libs/admin-console/src/common/index.ts @@ -1,2 +1,3 @@ -export * from "./organization-user"; +export * from "./auto-confirm"; export * from "./collections"; +export * from "./organization-user"; diff --git a/libs/admin-console/src/common/organization-user/abstractions/index.ts b/libs/admin-console/src/common/organization-user/abstractions/index.ts index 01cd189b3dd..dc2788deead 100644 --- a/libs/admin-console/src/common/organization-user/abstractions/index.ts +++ b/libs/admin-console/src/common/organization-user/abstractions/index.ts @@ -1 +1,2 @@ export * from "./organization-user-api.service"; +export * from "./organization-user.service"; diff --git a/libs/admin-console/src/common/organization-user/abstractions/organization-user-api.service.ts b/libs/admin-console/src/common/organization-user/abstractions/organization-user-api.service.ts index ff422231a12..71d228ff822 100644 --- a/libs/admin-console/src/common/organization-user/abstractions/organization-user-api.service.ts +++ b/libs/admin-console/src/common/organization-user/abstractions/organization-user-api.service.ts @@ -148,6 +148,19 @@ export abstract class OrganizationUserApiService { request: OrganizationUserConfirmRequest, ): Promise<void>; + /** + * Admin api for automatically confirming an organization user that + * has accepted their invitation + * @param organizationId - Identifier for the organization to confirm + * @param id - Organization user identifier + * @param request - Request details for confirming the user + */ + abstract postOrganizationUserAutoConfirm( + organizationId: string, + id: string, + request: OrganizationUserConfirmRequest, + ): Promise<void>; + /** * Retrieve a list of the specified users' public keys * @param organizationId - Identifier for the organization to accept diff --git a/libs/admin-console/src/common/organization-user/abstractions/organization-user.service.ts b/libs/admin-console/src/common/organization-user/abstractions/organization-user.service.ts new file mode 100644 index 00000000000..844a0f412be --- /dev/null +++ b/libs/admin-console/src/common/organization-user/abstractions/organization-user.service.ts @@ -0,0 +1,45 @@ +import { Observable } from "rxjs"; + +import { + OrganizationUserConfirmRequest, + OrganizationUserBulkResponse, +} from "@bitwarden/admin-console/common"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; + +export abstract class OrganizationUserService { + /** + * Builds a confirmation request for an organization user. + * @param organization - The organization the user belongs to + * @param publicKey - The user's public key + * @returns An observable that emits the confirmation request + */ + abstract buildConfirmRequest( + organization: Organization, + publicKey: Uint8Array, + ): Observable<OrganizationUserConfirmRequest>; + + /** + * Confirms a user in an organization. + * @param organization - The organization the user belongs to + * @param userId - The ID of the user to confirm + * @param publicKey - The user's public key + * @returns An observable that completes when the user is confirmed + */ + abstract confirmUser( + organization: Organization, + userId: string, + publicKey: Uint8Array, + ): Observable<void>; + + /** + * Confirms multiple users in an organization. + * @param organization - The organization the users belong to + * @param userIdsWithKeys - Array of user IDs with their encrypted keys + * @returns An observable that emits the bulk confirmation response + */ + abstract bulkConfirmUsers( + organization: Organization, + userIdsWithKeys: { id: string; key: string }[], + ): Observable<ListResponse<OrganizationUserBulkResponse>>; +} diff --git a/libs/admin-console/src/common/organization-user/services/default-organization-user-api.service.ts b/libs/admin-console/src/common/organization-user/services/default-organization-user-api.service.ts index c16fba258ec..869d84a8c8e 100644 --- a/libs/admin-console/src/common/organization-user/services/default-organization-user-api.service.ts +++ b/libs/admin-console/src/common/organization-user/services/default-organization-user-api.service.ts @@ -194,6 +194,20 @@ export class DefaultOrganizationUserApiService implements OrganizationUserApiSer ); } + postOrganizationUserAutoConfirm( + organizationId: string, + id: string, + request: OrganizationUserConfirmRequest, + ): Promise<void> { + return this.apiService.send( + "POST", + "/organizations/" + organizationId + "/users/" + id + "/auto-confirm", + request, + true, + false, + ); + } + async postOrganizationUsersPublicKey( organizationId: string, ids: string[], diff --git a/apps/web/src/app/admin-console/organizations/members/services/organization-user/organization-user.service.spec.ts b/libs/admin-console/src/common/organization-user/services/default-organization-user.service.spec.ts similarity index 91% rename from apps/web/src/app/admin-console/organizations/members/services/organization-user/organization-user.service.spec.ts rename to libs/admin-console/src/common/organization-user/services/default-organization-user.service.spec.ts index 2ae5aa4eb98..982fb3ca5e0 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/organization-user/organization-user.service.spec.ts +++ b/libs/admin-console/src/common/organization-user/services/default-organization-user.service.spec.ts @@ -19,12 +19,10 @@ import { OrganizationId } from "@bitwarden/common/types/guid"; import { OrgKey } from "@bitwarden/common/types/key"; import { KeyService } from "@bitwarden/key-management"; -import { OrganizationUserView } from "../../../core/views/organization-user.view"; +import { DefaultOrganizationUserService } from "./default-organization-user.service"; -import { OrganizationUserService } from "./organization-user.service"; - -describe("OrganizationUserService", () => { - let service: OrganizationUserService; +describe("DefaultOrganizationUserService", () => { + let service: DefaultOrganizationUserService; let keyService: jest.Mocked<KeyService>; let encryptService: jest.Mocked<EncryptService>; let organizationUserApiService: jest.Mocked<OrganizationUserApiService>; @@ -34,9 +32,7 @@ describe("OrganizationUserService", () => { const mockOrganization = new Organization(); mockOrganization.id = "org-123" as OrganizationId; - const mockOrganizationUser = new OrganizationUserView(); - mockOrganizationUser.id = "user-123"; - + const mockUserId = "user-123"; const mockPublicKey = new Uint8Array(64) as CsprngArray; const mockRandomBytes = new Uint8Array(64) as CsprngArray; const mockOrgKey = new SymmetricCryptoKey(mockRandomBytes) as OrgKey; @@ -77,7 +73,7 @@ describe("OrganizationUserService", () => { TestBed.configureTestingModule({ providers: [ - OrganizationUserService, + DefaultOrganizationUserService, { provide: KeyService, useValue: keyService }, { provide: EncryptService, useValue: encryptService }, { provide: OrganizationUserApiService, useValue: organizationUserApiService }, @@ -86,7 +82,13 @@ describe("OrganizationUserService", () => { ], }); - service = TestBed.inject(OrganizationUserService); + service = new DefaultOrganizationUserService( + keyService, + encryptService, + organizationUserApiService, + accountService, + i18nService, + ); }); describe("confirmUser", () => { @@ -97,7 +99,7 @@ describe("OrganizationUserService", () => { }); it("should confirm a user successfully", (done) => { - service.confirmUser(mockOrganization, mockOrganizationUser, mockPublicKey).subscribe({ + service.confirmUser(mockOrganization, mockUserId, mockPublicKey).subscribe({ next: () => { expect(i18nService.t).toHaveBeenCalledWith("myItems"); @@ -112,7 +114,7 @@ describe("OrganizationUserService", () => { expect(organizationUserApiService.postOrganizationUserConfirm).toHaveBeenCalledWith( mockOrganization.id, - mockOrganizationUser.id, + mockUserId, { key: mockEncryptedKey.encryptedString, defaultUserCollectionName: mockEncryptedCollectionName.encryptedString, diff --git a/apps/web/src/app/admin-console/organizations/members/services/organization-user/organization-user.service.ts b/libs/admin-console/src/common/organization-user/services/default-organization-user.service.ts similarity index 80% rename from apps/web/src/app/admin-console/organizations/members/services/organization-user/organization-user.service.ts rename to libs/admin-console/src/common/organization-user/services/default-organization-user.service.ts index f59b377e26e..4f503a92675 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/organization-user/organization-user.service.ts +++ b/libs/admin-console/src/common/organization-user/services/default-organization-user.service.ts @@ -1,4 +1,3 @@ -import { Injectable } from "@angular/core"; import { combineLatest, filter, map, Observable, switchMap } from "rxjs"; import { @@ -6,6 +5,7 @@ import { OrganizationUserBulkConfirmRequest, OrganizationUserApiService, OrganizationUserBulkResponse, + OrganizationUserService, } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -16,12 +16,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { OrganizationId } from "@bitwarden/common/types/guid"; import { KeyService } from "@bitwarden/key-management"; -import { OrganizationUserView } from "../../../core/views/organization-user.view"; - -@Injectable({ - providedIn: "root", -}) -export class OrganizationUserService { +export class DefaultOrganizationUserService implements OrganizationUserService { constructor( protected keyService: KeyService, private encryptService: EncryptService, @@ -39,11 +34,10 @@ export class OrganizationUserService { ); } - confirmUser( + buildConfirmRequest( organization: Organization, - user: OrganizationUserView, publicKey: Uint8Array, - ): Observable<void> { + ): Observable<OrganizationUserConfirmRequest> { const encryptedCollectionName$ = this.getEncryptedDefaultCollectionName$(organization); const encryptedKey$ = this.orgKey$(organization).pipe( @@ -51,18 +45,22 @@ export class OrganizationUserService { ); return combineLatest([encryptedKey$, encryptedCollectionName$]).pipe( - switchMap(([key, collectionName]) => { - const request: OrganizationUserConfirmRequest = { - key: key.encryptedString, - defaultUserCollectionName: collectionName.encryptedString, - }; + map(([key, collectionName]) => ({ + key: key.encryptedString, + defaultUserCollectionName: collectionName.encryptedString, + })), + ); + } - return this.organizationUserApiService.postOrganizationUserConfirm( + confirmUser(organization: Organization, userId: string, publicKey: Uint8Array): Observable<void> { + return this.buildConfirmRequest(organization, publicKey).pipe( + switchMap((request) => + this.organizationUserApiService.postOrganizationUserConfirm( organization.id, - user.id, + userId, request, - ); - }), + ), + ), ); } diff --git a/libs/admin-console/src/common/organization-user/services/index.ts b/libs/admin-console/src/common/organization-user/services/index.ts index 6135236d6a6..929a9fcd39a 100644 --- a/libs/admin-console/src/common/organization-user/services/index.ts +++ b/libs/admin-console/src/common/organization-user/services/index.ts @@ -1 +1,2 @@ export * from "./default-organization-user-api.service"; +export * from "./default-organization-user.service"; From 6f34b6098ae4440272f17e1ba03fee0e33990ec8 Mon Sep 17 00:00:00 2001 From: Stephon Brown <sbrown@livefront.com> Date: Tue, 28 Oct 2025 10:51:30 -0400 Subject: [PATCH 61/73] [PM-27252] Upgrade Dialog Should not Show in Self Host (#17051) * fix(billing): update and refactor observable logic * tests(billing): add additional expects for dialog * fix(billing): update for claude feedback * tests(billing): update test conditions and comments --- .../unified-upgrade-prompt.service.spec.ts | 78 +++++++++++-- .../unified-upgrade-prompt.service.ts | 106 +++++++++--------- 2 files changed, 123 insertions(+), 61 deletions(-) diff --git a/apps/web/src/app/billing/individual/upgrade/services/unified-upgrade-prompt.service.spec.ts b/apps/web/src/app/billing/individual/upgrade/services/unified-upgrade-prompt.service.spec.ts index a0b71e598f6..ea74eb67ffc 100644 --- a/apps/web/src/app/billing/individual/upgrade/services/unified-upgrade-prompt.service.spec.ts +++ b/apps/web/src/app/billing/individual/upgrade/services/unified-upgrade-prompt.service.spec.ts @@ -1,6 +1,5 @@ import { mock, mockReset } from "jest-mock-extended"; -import * as rxjs from "rxjs"; -import { of } from "rxjs"; +import { of, BehaviorSubject } from "rxjs"; import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; @@ -8,6 +7,7 @@ import { AccountService, Account } from "@bitwarden/common/auth/abstractions/acc import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/platform/sync/sync.service"; import { DialogRef, DialogService } from "@bitwarden/components"; @@ -28,6 +28,7 @@ describe("UnifiedUpgradePromptService", () => { const mockDialogService = mock<DialogService>(); const mockOrganizationService = mock<OrganizationService>(); const mockDialogOpen = jest.spyOn(UnifiedUpgradeDialogComponent, "open"); + const mockPlatformUtilsService = mock<PlatformUtilsService>(); /** * Creates a mock DialogRef that implements the required properties for testing @@ -57,33 +58,33 @@ describe("UnifiedUpgradePromptService", () => { mockSyncService, mockDialogService, mockOrganizationService, + mockPlatformUtilsService, ); } const mockAccount: Account = { id: "test-user-id", } as Account; - const accountSubject = new rxjs.BehaviorSubject(mockAccount); + const accountSubject = new BehaviorSubject<Account | null>(mockAccount); describe("initialization", () => { beforeEach(() => { + mockAccountService.activeAccount$ = accountSubject.asObservable(); + mockPlatformUtilsService.isSelfHost.mockReturnValue(false); + mockConfigService.getFeatureFlag$.mockReturnValue(of(true)); + setupTestService(); }); it("should be created", () => { expect(sut).toBeTruthy(); }); - - it("should subscribe to account and feature flag observables on construction", () => { - expect(mockConfigService.getFeatureFlag$).toHaveBeenCalledWith( - FeatureFlag.PM24996_ImplementUpgradeFromFreeDialog, - ); - }); }); describe("displayUpgradePromptConditionally", () => { - beforeEach(async () => { + beforeEach(() => { mockAccountService.activeAccount$ = accountSubject.asObservable(); mockDialogOpen.mockReset(); + mockReset(mockDialogService); mockReset(mockConfigService); mockReset(mockBillingService); mockReset(mockVaultProfileService); @@ -93,20 +94,48 @@ describe("UnifiedUpgradePromptService", () => { // Mock sync service methods mockSyncService.fullSync.mockResolvedValue(true); mockSyncService.lastSync$.mockReturnValue(of(new Date())); + mockReset(mockPlatformUtilsService); + }); + it("should subscribe to account and feature flag observables when checking display conditions", async () => { + // Arrange + mockPlatformUtilsService.isSelfHost.mockReturnValue(false); + mockOrganizationService.memberOrganizations$.mockReturnValue(of([])); + mockConfigService.getFeatureFlag$.mockReturnValue(of(false)); + mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(false)); + + setupTestService(); + + // Act + await sut.displayUpgradePromptConditionally(); + + // Assert + expect(mockConfigService.getFeatureFlag$).toHaveBeenCalledWith( + FeatureFlag.PM24996_ImplementUpgradeFromFreeDialog, + ); + expect(mockAccountService.activeAccount$).toBeDefined(); }); it("should not show dialog when feature flag is disabled", async () => { // Arrange + mockPlatformUtilsService.isSelfHost.mockReturnValue(false); + mockOrganizationService.memberOrganizations$.mockReturnValue(of([])); mockConfigService.getFeatureFlag$.mockReturnValue(of(false)); + mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(false)); + const recentDate = new Date(); + recentDate.setMinutes(recentDate.getMinutes() - 3); // 3 minutes old + mockVaultProfileService.getProfileCreationDate.mockResolvedValue(recentDate); + setupTestService(); // Act const result = await sut.displayUpgradePromptConditionally(); // Assert expect(result).toBeNull(); + expect(mockDialogOpen).not.toHaveBeenCalled(); }); it("should not show dialog when user has premium", async () => { // Arrange + mockPlatformUtilsService.isSelfHost.mockReturnValue(false); mockConfigService.getFeatureFlag$.mockReturnValue(of(true)); mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(true)); mockOrganizationService.memberOrganizations$.mockReturnValue(of([])); @@ -117,6 +146,7 @@ describe("UnifiedUpgradePromptService", () => { // Assert expect(result).toBeNull(); + expect(mockDialogOpen).not.toHaveBeenCalled(); }); it("should not show dialog when user has any organization membership", async () => { @@ -124,6 +154,7 @@ describe("UnifiedUpgradePromptService", () => { mockConfigService.getFeatureFlag$.mockReturnValue(of(true)); mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(false)); mockOrganizationService.memberOrganizations$.mockReturnValue(of([{ id: "org1" } as any])); + mockPlatformUtilsService.isSelfHost.mockReturnValue(false); setupTestService(); // Act @@ -131,6 +162,7 @@ describe("UnifiedUpgradePromptService", () => { // Assert expect(result).toBeNull(); + expect(mockDialogOpen).not.toHaveBeenCalled(); }); it("should not show dialog when profile is older than 5 minutes", async () => { @@ -141,6 +173,7 @@ describe("UnifiedUpgradePromptService", () => { const oldDate = new Date(); oldDate.setMinutes(oldDate.getMinutes() - 10); // 10 minutes old mockVaultProfileService.getProfileCreationDate.mockResolvedValue(oldDate); + mockPlatformUtilsService.isSelfHost.mockReturnValue(false); setupTestService(); // Act @@ -148,6 +181,7 @@ describe("UnifiedUpgradePromptService", () => { // Assert expect(result).toBeNull(); + expect(mockDialogOpen).not.toHaveBeenCalled(); }); it("should show dialog when all conditions are met", async () => { @@ -158,6 +192,7 @@ describe("UnifiedUpgradePromptService", () => { const recentDate = new Date(); recentDate.setMinutes(recentDate.getMinutes() - 3); // 3 minutes old mockVaultProfileService.getProfileCreationDate.mockResolvedValue(recentDate); + mockPlatformUtilsService.isSelfHost.mockReturnValue(false); const expectedResult = { status: UnifiedUpgradeDialogStatus.Closed }; mockDialogOpenMethod(createMockDialogRef(expectedResult)); @@ -182,6 +217,7 @@ describe("UnifiedUpgradePromptService", () => { // Assert expect(result).toBeNull(); + expect(mockDialogOpen).not.toHaveBeenCalled(); }); it("should not show dialog when profile creation date is unavailable", async () => { @@ -190,6 +226,8 @@ describe("UnifiedUpgradePromptService", () => { mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(false)); mockOrganizationService.memberOrganizations$.mockReturnValue(of([])); mockVaultProfileService.getProfileCreationDate.mockResolvedValue(null); + mockPlatformUtilsService.isSelfHost.mockReturnValue(false); + setupTestService(); // Act @@ -197,6 +235,26 @@ describe("UnifiedUpgradePromptService", () => { // Assert expect(result).toBeNull(); + expect(mockDialogOpen).not.toHaveBeenCalled(); + }); + + it("should not show dialog when running in self-hosted environment", async () => { + // Arrange + mockConfigService.getFeatureFlag$.mockReturnValue(of(true)); + mockOrganizationService.memberOrganizations$.mockReturnValue(of([])); + mockBillingService.hasPremiumFromAnySource$.mockReturnValue(of(false)); + const recentDate = new Date(); + recentDate.setMinutes(recentDate.getMinutes() - 3); // 3 minutes old + mockVaultProfileService.getProfileCreationDate.mockResolvedValue(recentDate); + mockPlatformUtilsService.isSelfHost.mockReturnValue(true); + setupTestService(); + + // Act + const result = await sut.displayUpgradePromptConditionally(); + + // Assert + expect(result).toBeNull(); + expect(mockDialogOpen).not.toHaveBeenCalled(); }); }); }); diff --git a/apps/web/src/app/billing/individual/upgrade/services/unified-upgrade-prompt.service.ts b/apps/web/src/app/billing/individual/upgrade/services/unified-upgrade-prompt.service.ts index 8dd7f31275c..cf5deaf37fa 100644 --- a/apps/web/src/app/billing/individual/upgrade/services/unified-upgrade-prompt.service.ts +++ b/apps/web/src/app/billing/individual/upgrade/services/unified-upgrade-prompt.service.ts @@ -1,6 +1,6 @@ import { Injectable } from "@angular/core"; -import { combineLatest, firstValueFrom, timeout } from "rxjs"; -import { filter, switchMap, take } from "rxjs/operators"; +import { combineLatest, firstValueFrom, timeout, from, Observable, of } from "rxjs"; +import { filter, switchMap, take, map } from "rxjs/operators"; import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; @@ -8,7 +8,9 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/platform/sync/sync.service"; +import { UserId } from "@bitwarden/common/types/guid"; import { DialogRef, DialogService } from "@bitwarden/components"; import { @@ -29,63 +31,37 @@ export class UnifiedUpgradePromptService { private syncService: SyncService, private dialogService: DialogService, private organizationService: OrganizationService, + private platformUtilsService: PlatformUtilsService, ) {} - private shouldShowPrompt$ = combineLatest([ - this.accountService.activeAccount$, - this.configService.getFeatureFlag$(FeatureFlag.PM24996_ImplementUpgradeFromFreeDialog), - ]).pipe( - switchMap(async ([account, isFlagEnabled]) => { - if (!account || !account?.id) { - return false; - } - // Early return if feature flag is disabled - if (!isFlagEnabled) { - return false; + private shouldShowPrompt$: Observable<boolean> = this.accountService.activeAccount$.pipe( + switchMap((account) => { + // Check self-hosted first before any other operations + if (this.platformUtilsService.isSelfHost()) { + return of(false); } - // Wait for sync to complete to ensure organizations are fully loaded - // Also force a sync to ensure we have the latest data - await this.syncService.fullSync(false); + if (!account) { + return of(false); + } - // Wait for the sync to complete with timeout to prevent hanging - await firstValueFrom( - this.syncService.lastSync$(account.id).pipe( - filter((lastSync) => lastSync !== null), - take(1), - timeout(30000), // 30 second timeout - ), + const isProfileLessThanFiveMinutesOld = from( + this.isProfileLessThanFiveMinutesOld(account.id), ); + const hasOrganizations = from(this.hasOrganizations(account.id)); - // Check if user has premium - const hasPremium = await firstValueFrom( + return combineLatest([ + isProfileLessThanFiveMinutesOld, + hasOrganizations, this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), + this.configService.getFeatureFlag$(FeatureFlag.PM24996_ImplementUpgradeFromFreeDialog), + ]).pipe( + map(([isProfileLessThanFiveMinutesOld, hasOrganizations, hasPremium, isFlagEnabled]) => { + return ( + isProfileLessThanFiveMinutesOld && !hasOrganizations && !hasPremium && isFlagEnabled + ); + }), ); - - // Early return if user already has premium - if (hasPremium) { - return false; - } - - // Check if user has any organization membership (any status including pending) - // Try using memberOrganizations$ which might have different filtering logic - const memberOrganizations = await firstValueFrom( - this.organizationService.memberOrganizations$(account.id), - ); - - const hasOrganizations = memberOrganizations.length > 0; - - // Early return if user has any organization status - if (hasOrganizations) { - return false; - } - - // Check profile age only if needed - const isProfileLessThanFiveMinutesOld = await this.isProfileLessThanFiveMinutesOld( - account.id, - ); - - return isFlagEnabled && !hasPremium && !hasOrganizations && isProfileLessThanFiveMinutesOld; }), take(1), ); @@ -119,7 +95,7 @@ export class UnifiedUpgradePromptService { const nowInMs = new Date().getTime(); const differenceInMs = nowInMs - createdAtInMs; - const msInAMinute = 1000 * 60; // Milliseconds in a minute for conversion 1 minute = 60 seconds * 1000 ms + const msInAMinute = 1000 * 60; // 60 seconds * 1000ms const differenceInMinutes = Math.round(differenceInMs / msInAMinute); return differenceInMinutes <= 5; @@ -141,4 +117,32 @@ export class UnifiedUpgradePromptService { // Return the result or null if the dialog was dismissed without a result return result || null; } + + /** + * Checks if the user has any organization associated with their account + * @param userId User ID to check + * @returns Promise that resolves to true if user has any organizations, false otherwise + */ + private async hasOrganizations(userId: UserId): Promise<boolean> { + // Wait for sync to complete to ensure organizations are fully loaded + // Also force a sync to ensure we have the latest data + await this.syncService.fullSync(false); + + // Wait for the sync to complete with timeout to prevent hanging + await firstValueFrom( + this.syncService.lastSync$(userId).pipe( + filter((lastSync) => lastSync !== null), + take(1), + timeout(30000), // 30 second timeout + ), + ); + + // Check if user has any organization membership (any status including pending) + // Try using memberOrganizations$ which might have different filtering logic + const memberOrganizations = await firstValueFrom( + this.organizationService.memberOrganizations$(userId), + ); + + return memberOrganizations.length > 0; + } } From 6505ce05db7f0aa77ca693fdba79877492574567 Mon Sep 17 00:00:00 2001 From: Alex <55413326+AlexRubik@users.noreply.github.com> Date: Tue, 28 Oct 2025 11:03:11 -0400 Subject: [PATCH 62/73] [PM-27162] Add runtime type guards for decrypted JSON data (#16996) * Add runtime type guards for decrypted JSON data - Create risk-insights-type-guards.ts with validation functions - Replace unsafe type assertions with runtime validation in encryption service - Validate ApplicationHealthReportDetail, OrganizationReportSummary, and OrganizationReportApplication - Add detailed error messages for validation failures - Remove TODO comments for type guard implementation Improves security by preventing malformed data from bypassing type safety and ensures data integrity for decrypted report structures. * test file fix * date validation * add runtime type guards and validation failure tests Issue 1: Missing Test Coverage for Type Guard Validation Failures - Create comprehensive test suite with 17 tests covering all validation scenarios - Test invalid structures, missing fields, wrong types, and edge cases - Verify proper error messages and validation logic for all data types Issue 2: Silent Failure on Validation Errors (Security Concern) - Re-throw validation errors instead of silently returning empty/default data - Add descriptive error messages indicating potential data corruption or tampering - Ensure all validation failures are surfaced as security issues, not swallowed Additional Fix: Date Validation Vulnerability - Validate date strings before creating Date objects to prevent Invalid Date (NaN) - Throw explicit errors for unparseable date strings - Update error handling to catch and properly surface date validation failures * add empty string validation and sanitize error messages - Validate array elements are non-empty strings (atRiskCipherIds, cipherIds, newApplications) - Sanitize validation error messages to prevent information disclosure - Log detailed errors for debugging, re-throw generic messages - Add tests for empty string validation and error message sanitization * add comprehensive validation for scalar strings and numeric ranges - Validate all scalar string fields are non-empty (applicationName, userName, email, cipherId, userGuid) - Add numeric range validation (finite, non-negative) for all count fields - Export type guard functions for testability and reusability - Add 19 new tests covering edge cases (empty strings, NaN, Infinity, negative numbers) * prevent prototype pollution and unexpected property injection in type guards - Validate object prototype is Object.prototype (prevents __proto__ attacks) - Check for dangerous own properties (constructor, prototype) - Strict property enumeration - reject objects with unexpected properties - Add comprehensive security tests (prototype pollution, unexpected props) - Protects against data tampering and information leakage * security: always sanitize error messages to prevent information disclosure - Remove fragile pattern matching in error handlers - Always throw generic error messages by default - Log detailed errors for debugging, never expose to callers - Future-proof against validation error message changes - Prevents disclosure of internal data structure details Applies to all decryption/validation methods in encryption service * security: comprehensive hardening of type validation system CRITICAL FIXES: - Add __proto__ to prototype pollution checks (loop-based) - Remove conditional error sanitization (always sanitize) SECURITY ENHANCEMENTS: - Add integer overflow protection (Number.isSafeInteger) - Add DoS prevention (array/string length limits: 50K/1K) - Strengthen all 4 type guards with 10-layer validation LIMITS: - Max string length: 1,000 characters - Max array length: 50,000 elements - Max safe integer: 2^53 - 1 DOCUMENTATION: - Update code-review-methodology.md with patterns - Update .cursorrules with security best practices - Create comprehensive security audit document All 57 tests passing. No linting errors. Defense-in-depth complete - production ready. * fix: consolidate security constants and add upper bound validation CRITICAL FIXES: - Consolidate MAX_STRING_LENGTH and MAX_ARRAY_LENGTH to file level (DRY) - Add MAX_COUNT constant (10M) for upper bound validation - Apply upper bound checks to all 12 count fields BENEFITS: - Single source of truth for security limits - Prevents business logic issues from extreme values - Easier maintenance and updates --- .../risk-insights-encryption.service.spec.ts | 144 +++- .../risk-insights-encryption.service.ts | 47 +- .../domain/risk-insights-type-guards.spec.ts | 668 ++++++++++++++++++ .../domain/risk-insights-type-guards.ts | 404 +++++++++++ 4 files changed, 1232 insertions(+), 31 deletions(-) create mode 100644 bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-type-guards.spec.ts create mode 100644 bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-type-guards.ts diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-encryption.service.spec.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-encryption.service.spec.ts index 2efd97b3c30..b9b2cd8de97 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-encryption.service.spec.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-encryption.service.spec.ts @@ -151,14 +151,17 @@ describe("RiskInsightsEncryptionService", () => { describe("decryptRiskInsightsReport", () => { it("should decrypt data and return original object", async () => { - // Arrange: setup our mocks + // Arrange: setup our mocks with valid data structures mockKeyService.orgKeys$.mockReturnValue(orgKey$); mockEncryptService.unwrapSymmetricKey.mockResolvedValue(contentEncryptionKey); - mockEncryptService.decryptString.mockResolvedValue(JSON.stringify(testData)); - // act: call the decrypt method - with any params - // actual decryption does not happen here, - // we just want to ensure the method calls are correct + // Mock decryption to return valid data for each call + mockEncryptService.decryptString + .mockResolvedValueOnce(JSON.stringify(mockReportData)) + .mockResolvedValueOnce(JSON.stringify(mockSummaryData)) + .mockResolvedValueOnce(JSON.stringify(mockApplicationData)); + + // act: call the decrypt method const result = await service.decryptRiskInsightsReport( { organizationId: orgId, userId }, mockEncryptedData, @@ -169,33 +172,37 @@ describe("RiskInsightsEncryptionService", () => { expect(mockEncryptService.unwrapSymmetricKey).toHaveBeenCalledWith(mockKey, orgKey); expect(mockEncryptService.decryptString).toHaveBeenCalledTimes(3); - // Mock decrypt returns JSON.stringify(testData) + // Verify decrypted data matches the mocked valid data expect(result).toEqual({ - reportData: testData, - summaryData: testData, - applicationData: testData, + reportData: mockReportData, + summaryData: mockSummaryData, + applicationData: mockApplicationData, }); }); it("should invoke data type validation method during decryption", async () => { - // Arrange: setup our mocks + // Arrange: setup our mocks with valid data structures mockKeyService.orgKeys$.mockReturnValue(orgKey$); mockEncryptService.unwrapSymmetricKey.mockResolvedValue(contentEncryptionKey); - mockEncryptService.decryptString.mockResolvedValue(JSON.stringify(testData)); - // act: call the decrypt method - with any params - // actual decryption does not happen here, - // we just want to ensure the method calls are correct + // Mock decryption to return valid data for each call + mockEncryptService.decryptString + .mockResolvedValueOnce(JSON.stringify(mockReportData)) + .mockResolvedValueOnce(JSON.stringify(mockSummaryData)) + .mockResolvedValueOnce(JSON.stringify(mockApplicationData)); + + // act: call the decrypt method const result = await service.decryptRiskInsightsReport( { organizationId: orgId, userId }, mockEncryptedData, mockKey, ); + // Verify that validation passed and returned the correct data expect(result).toEqual({ - reportData: testData, - summaryData: testData, - applicationData: testData, + reportData: mockReportData, + summaryData: mockSummaryData, + applicationData: mockApplicationData, }); }); @@ -211,7 +218,7 @@ describe("RiskInsightsEncryptionService", () => { ).rejects.toEqual(Error("Organization key not found")); }); - it("should return null if decrypt throws", async () => { + it("should throw if decrypt throws", async () => { mockKeyService.orgKeys$.mockReturnValue(orgKey$); mockEncryptService.unwrapSymmetricKey.mockRejectedValue(new Error("fail")); @@ -224,5 +231,106 @@ describe("RiskInsightsEncryptionService", () => { ), ).rejects.toEqual(Error("fail")); }); + + it("should throw error when report data validation fails", async () => { + mockKeyService.orgKeys$.mockReturnValue(orgKey$); + mockEncryptService.unwrapSymmetricKey.mockResolvedValue(contentEncryptionKey); + + // Mock decryption to return invalid data + mockEncryptService.decryptString + .mockResolvedValueOnce(JSON.stringify([{ invalid: "data" }])) // invalid report data + .mockResolvedValueOnce(JSON.stringify(mockSummaryData)) + .mockResolvedValueOnce(JSON.stringify(mockApplicationData)); + + await expect( + service.decryptRiskInsightsReport( + { organizationId: orgId, userId }, + mockEncryptedData, + mockKey, + ), + ).rejects.toThrow( + /Report data validation failed.*This may indicate data corruption or tampering/, + ); + }); + + it("should throw error when summary data validation fails", async () => { + mockKeyService.orgKeys$.mockReturnValue(orgKey$); + mockEncryptService.unwrapSymmetricKey.mockResolvedValue(contentEncryptionKey); + + // Clear and reset the mock + mockEncryptService.decryptString.mockReset(); + + // Mock decryption - report data should succeed, summary should fail + mockEncryptService.decryptString + .mockResolvedValueOnce(JSON.stringify(mockReportData)) // valid + .mockResolvedValueOnce(JSON.stringify({ invalid: "summary" })) // invalid summary data - fails here + .mockResolvedValueOnce(JSON.stringify(mockApplicationData)); // won't be called but prevents fallback + + await expect( + service.decryptRiskInsightsReport( + { organizationId: orgId, userId }, + mockEncryptedData, + mockKey, + ), + ).rejects.toThrow( + /Summary data validation failed.*This may indicate data corruption or tampering/, + ); + }); + + it("should throw error when application data validation fails", async () => { + mockKeyService.orgKeys$.mockReturnValue(orgKey$); + mockEncryptService.unwrapSymmetricKey.mockResolvedValue(contentEncryptionKey); + + // Clear and reset the mock + mockEncryptService.decryptString.mockReset(); + + // Mock decryption - report and summary should succeed, application should fail + mockEncryptService.decryptString + .mockResolvedValueOnce(JSON.stringify(mockReportData)) // valid + .mockResolvedValueOnce(JSON.stringify(mockSummaryData)) // valid + .mockResolvedValueOnce(JSON.stringify([{ invalid: "application" }])); // invalid app data + + await expect( + service.decryptRiskInsightsReport( + { organizationId: orgId, userId }, + mockEncryptedData, + mockKey, + ), + ).rejects.toThrow( + /Application data validation failed.*This may indicate data corruption or tampering/, + ); + }); + + it("should throw error for invalid date in application data", async () => { + mockKeyService.orgKeys$.mockReturnValue(orgKey$); + mockEncryptService.unwrapSymmetricKey.mockResolvedValue(contentEncryptionKey); + + const invalidApplicationData = [ + { + applicationName: "Test App", + isCritical: true, + reviewedDate: "invalid-date-string", + }, + ]; + + // Clear and reset the mock + mockEncryptService.decryptString.mockReset(); + + // Mock decryption - report and summary succeed, application with invalid date fails + mockEncryptService.decryptString + .mockResolvedValueOnce(JSON.stringify(mockReportData)) // valid + .mockResolvedValueOnce(JSON.stringify(mockSummaryData)) // valid + .mockResolvedValueOnce(JSON.stringify(invalidApplicationData)); // invalid date + + await expect( + service.decryptRiskInsightsReport( + { organizationId: orgId, userId }, + mockEncryptedData, + mockKey, + ), + ).rejects.toThrow( + /Application data validation failed.*This may indicate data corruption or tampering/, + ); + }); }); }); diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-encryption.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-encryption.service.ts index 5206cd1ecff..abeae1fdb29 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-encryption.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-encryption.service.ts @@ -10,14 +10,20 @@ import { LogService } from "@bitwarden/logging"; import { createNewSummaryData } from "../../helpers"; import { - DecryptedReportData, - EncryptedReportData, - EncryptedDataWithKey, ApplicationHealthReportDetail, - OrganizationReportSummary, + DecryptedReportData, + EncryptedDataWithKey, + EncryptedReportData, OrganizationReportApplication, + OrganizationReportSummary, } from "../../models"; +import { + validateApplicationHealthReportDetailArray, + validateOrganizationReportApplicationArray, + validateOrganizationReportSummary, +} from "./risk-insights-type-guards"; + export class RiskInsightsEncryptionService { constructor( private keyService: KeyService, @@ -182,11 +188,16 @@ export class RiskInsightsEncryptionService { const decryptedData = await this.encryptService.decryptString(encryptedData, key); const parsedData = JSON.parse(decryptedData); - // TODO Add type guard to check that parsed data is actual type - return parsedData as ApplicationHealthReportDetail[]; + // Validate parsed data structure with runtime type guards + return validateApplicationHealthReportDetailArray(parsedData); } catch (error: unknown) { + // Log detailed error for debugging this.logService.error("[RiskInsightsEncryptionService] Failed to decrypt report", error); - return []; + // Always throw generic message to prevent information disclosure + // Original error with detailed validation info is logged, not exposed to caller + throw new Error( + "Report data validation failed. This may indicate data corruption or tampering.", + ); } } @@ -202,14 +213,19 @@ export class RiskInsightsEncryptionService { const decryptedData = await this.encryptService.decryptString(encryptedData, key); const parsedData = JSON.parse(decryptedData); - // TODO Add type guard to check that parsed data is actual type - return parsedData as OrganizationReportSummary; + // Validate parsed data structure with runtime type guards + return validateOrganizationReportSummary(parsedData); } catch (error: unknown) { + // Log detailed error for debugging this.logService.error( "[RiskInsightsEncryptionService] Failed to decrypt report summary", error, ); - return createNewSummaryData(); + // Always throw generic message to prevent information disclosure + // Original error with detailed validation info is logged, not exposed to caller + throw new Error( + "Summary data validation failed. This may indicate data corruption or tampering.", + ); } } @@ -225,14 +241,19 @@ export class RiskInsightsEncryptionService { const decryptedData = await this.encryptService.decryptString(encryptedData, key); const parsedData = JSON.parse(decryptedData); - // TODO Add type guard to check that parsed data is actual type - return parsedData as OrganizationReportApplication[]; + // Validate parsed data structure with runtime type guards + return validateOrganizationReportApplicationArray(parsedData); } catch (error: unknown) { + // Log detailed error for debugging this.logService.error( "[RiskInsightsEncryptionService] Failed to decrypt report applications", error, ); - return []; + // Always throw generic message to prevent information disclosure + // Original error with detailed validation info is logged, not exposed to caller + throw new Error( + "Application data validation failed. This may indicate data corruption or tampering.", + ); } } } diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-type-guards.spec.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-type-guards.spec.ts new file mode 100644 index 00000000000..32505088818 --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-type-guards.spec.ts @@ -0,0 +1,668 @@ +import { MemberDetails } from "../../models"; + +import { + isApplicationHealthReportDetail, + isMemberDetails, + isOrganizationReportApplication, + isOrganizationReportSummary, + validateApplicationHealthReportDetailArray, + validateOrganizationReportApplicationArray, + validateOrganizationReportSummary, +} from "./risk-insights-type-guards"; + +describe("Risk Insights Type Guards", () => { + describe("validateApplicationHealthReportDetailArray", () => { + it("should validate valid ApplicationHealthReportDetail array", () => { + const validData = [ + { + applicationName: "Test App", + passwordCount: 10, + atRiskPasswordCount: 2, + atRiskCipherIds: ["cipher-1", "cipher-2"], + memberCount: 5, + atRiskMemberCount: 1, + memberDetails: [ + { + userGuid: "user-1", + userName: "John Doe", + email: "john@example.com", + cipherId: "cipher-1", + }, + ], + atRiskMemberDetails: [ + { + userGuid: "user-2", + userName: "Jane Doe", + email: "jane@example.com", + cipherId: "cipher-2", + }, + ], + cipherIds: ["cipher-1", "cipher-2"], + }, + ]; + + expect(() => validateApplicationHealthReportDetailArray(validData)).not.toThrow(); + expect(validateApplicationHealthReportDetailArray(validData)).toEqual(validData); + }); + + it("should throw error for non-array input", () => { + expect(() => validateApplicationHealthReportDetailArray("not an array")).toThrow( + "Invalid report data: expected array of ApplicationHealthReportDetail, received non-array", + ); + }); + + it("should throw error for array with invalid elements", () => { + const invalidData = [ + { + applicationName: "Test App", + // missing required fields + }, + ]; + + expect(() => validateApplicationHealthReportDetailArray(invalidData)).toThrow( + /Invalid report data: array contains 1 invalid ApplicationHealthReportDetail element\(s\) at indices: 0/, + ); + }); + + it("should throw error for array with multiple invalid elements", () => { + const invalidData = [ + { applicationName: "App 1" }, // invalid + { + applicationName: "App 2", + passwordCount: 10, + atRiskPasswordCount: 2, + atRiskCipherIds: ["cipher-1"], + memberCount: 5, + atRiskMemberCount: 1, + memberDetails: [] as MemberDetails[], + atRiskMemberDetails: [] as MemberDetails[], + cipherIds: ["cipher-1"], + }, // valid + { applicationName: "App 3" }, // invalid + ]; + + expect(() => validateApplicationHealthReportDetailArray(invalidData)).toThrow( + /Invalid report data: array contains 2 invalid ApplicationHealthReportDetail element\(s\) at indices: 0, 2/, + ); + }); + + it("should throw error for invalid memberDetails", () => { + const invalidData = [ + { + applicationName: "Test App", + passwordCount: 10, + atRiskPasswordCount: 2, + atRiskCipherIds: ["cipher-1"], + memberCount: 5, + atRiskMemberCount: 1, + memberDetails: [{ userGuid: "user-1" }] as any, // missing required fields + atRiskMemberDetails: [] as MemberDetails[], + cipherIds: ["cipher-1"], + }, + ]; + + expect(() => validateApplicationHealthReportDetailArray(invalidData)).toThrow( + /Invalid report data/, + ); + }); + + it("should throw error for empty string in atRiskCipherIds", () => { + const invalidData = [ + { + applicationName: "Test App", + passwordCount: 10, + atRiskPasswordCount: 2, + atRiskCipherIds: ["cipher-1", "", "cipher-3"], // empty string + memberCount: 5, + atRiskMemberCount: 1, + memberDetails: [] as MemberDetails[], + atRiskMemberDetails: [] as MemberDetails[], + cipherIds: ["cipher-1"], + }, + ]; + + expect(() => validateApplicationHealthReportDetailArray(invalidData)).toThrow( + /Invalid report data/, + ); + }); + + it("should throw error for empty string in cipherIds", () => { + const invalidData = [ + { + applicationName: "Test App", + passwordCount: 10, + atRiskPasswordCount: 2, + atRiskCipherIds: ["cipher-1"], + memberCount: 5, + atRiskMemberCount: 1, + memberDetails: [] as MemberDetails[], + atRiskMemberDetails: [] as MemberDetails[], + cipherIds: ["", "cipher-2"], // empty string + }, + ]; + + expect(() => validateApplicationHealthReportDetailArray(invalidData)).toThrow( + /Invalid report data/, + ); + }); + }); + + describe("validateOrganizationReportSummary", () => { + it("should validate valid OrganizationReportSummary", () => { + const validData = { + totalMemberCount: 10, + totalApplicationCount: 5, + totalAtRiskMemberCount: 2, + totalAtRiskApplicationCount: 1, + totalCriticalApplicationCount: 3, + totalCriticalMemberCount: 4, + totalCriticalAtRiskMemberCount: 1, + totalCriticalAtRiskApplicationCount: 1, + newApplications: ["app-1", "app-2"], + }; + + expect(() => validateOrganizationReportSummary(validData)).not.toThrow(); + expect(validateOrganizationReportSummary(validData)).toEqual(validData); + }); + + it("should throw error for missing totalMemberCount", () => { + const invalidData = { + totalApplicationCount: 5, + totalAtRiskMemberCount: 2, + totalAtRiskApplicationCount: 1, + totalCriticalApplicationCount: 3, + totalCriticalMemberCount: 4, + totalCriticalAtRiskMemberCount: 1, + totalCriticalAtRiskApplicationCount: 1, + newApplications: ["app-1"], + }; + + expect(() => validateOrganizationReportSummary(invalidData)).toThrow( + /Invalid OrganizationReportSummary: missing or invalid fields: totalMemberCount \(number\)/, + ); + }); + + it("should throw error for multiple missing fields", () => { + const invalidData = { + totalMemberCount: 10, + // missing multiple fields + newApplications: ["app-1"], + }; + + expect(() => validateOrganizationReportSummary(invalidData)).toThrow( + /Invalid OrganizationReportSummary: missing or invalid fields:.*totalApplicationCount/, + ); + }); + + it("should throw error for invalid field types", () => { + const invalidData = { + totalMemberCount: "10", // should be number + totalApplicationCount: 5, + totalAtRiskMemberCount: 2, + totalAtRiskApplicationCount: 1, + totalCriticalApplicationCount: 3, + totalCriticalMemberCount: 4, + totalCriticalAtRiskMemberCount: 1, + totalCriticalAtRiskApplicationCount: 1, + newApplications: ["app-1"], + }; + + expect(() => validateOrganizationReportSummary(invalidData)).toThrow( + /Invalid OrganizationReportSummary/, + ); + }); + + it("should throw error for non-array newApplications", () => { + const invalidData = { + totalMemberCount: 10, + totalApplicationCount: 5, + totalAtRiskMemberCount: 2, + totalAtRiskApplicationCount: 1, + totalCriticalApplicationCount: 3, + totalCriticalMemberCount: 4, + totalCriticalAtRiskMemberCount: 1, + totalCriticalAtRiskApplicationCount: 1, + newApplications: "not-an-array", + }; + + expect(() => validateOrganizationReportSummary(invalidData)).toThrow( + /Invalid OrganizationReportSummary.*newApplications/, + ); + }); + + it("should throw error for empty string in newApplications", () => { + const invalidData = { + totalMemberCount: 10, + totalApplicationCount: 5, + totalAtRiskMemberCount: 2, + totalAtRiskApplicationCount: 1, + totalCriticalApplicationCount: 3, + totalCriticalMemberCount: 4, + totalCriticalAtRiskMemberCount: 1, + totalCriticalAtRiskApplicationCount: 1, + newApplications: ["app-1", "", "app-3"], // empty string + }; + + expect(() => validateOrganizationReportSummary(invalidData)).toThrow( + /Invalid OrganizationReportSummary/, + ); + }); + }); + + describe("validateOrganizationReportApplicationArray", () => { + it("should validate valid OrganizationReportApplication array", () => { + const validData = [ + { + applicationName: "Test App", + isCritical: true, + reviewedDate: null, + }, + { + applicationName: "Another App", + isCritical: false, + reviewedDate: new Date("2024-01-01"), + }, + ]; + + expect(() => validateOrganizationReportApplicationArray(validData)).not.toThrow(); + const result = validateOrganizationReportApplicationArray(validData); + expect(result[0].applicationName).toBe("Test App"); + expect(result[1].reviewedDate).toBeInstanceOf(Date); + }); + + it("should convert string dates to Date objects", () => { + const validData = [ + { + applicationName: "Test App", + isCritical: true, + reviewedDate: "2024-01-01T00:00:00.000Z", + }, + ]; + + const result = validateOrganizationReportApplicationArray(validData); + expect(result[0].reviewedDate).toBeInstanceOf(Date); + expect(result[0].reviewedDate?.toISOString()).toBe("2024-01-01T00:00:00.000Z"); + }); + + it("should throw error for invalid date strings", () => { + const invalidData = [ + { + applicationName: "Test App", + isCritical: true, + reviewedDate: "invalid-date", + }, + ]; + + expect(() => validateOrganizationReportApplicationArray(invalidData)).toThrow( + "Invalid date string: invalid-date", + ); + }); + + it("should throw error for non-array input", () => { + expect(() => validateOrganizationReportApplicationArray("not an array")).toThrow( + "Invalid application data: expected array of OrganizationReportApplication, received non-array", + ); + }); + + it("should throw error for array with invalid elements", () => { + const invalidData = [ + { + applicationName: "Test App", + reviewedDate: null as any, + // missing isCritical field + } as any, + ]; + + expect(() => validateOrganizationReportApplicationArray(invalidData)).toThrow( + /Invalid application data: array contains 1 invalid OrganizationReportApplication element\(s\) at indices: 0/, + ); + }); + + it("should throw error for invalid field types", () => { + const invalidData = [ + { + applicationName: 123 as any, // should be string + isCritical: true, + reviewedDate: null as any, + } as any, + ]; + + expect(() => validateOrganizationReportApplicationArray(invalidData)).toThrow( + /Invalid application data/, + ); + }); + + it("should accept null reviewedDate", () => { + const validData = [ + { + applicationName: "Test App", + isCritical: true, + reviewedDate: null as any, + }, + ]; + + expect(() => validateOrganizationReportApplicationArray(validData)).not.toThrow(); + const result = validateOrganizationReportApplicationArray(validData); + expect(result[0].reviewedDate).toBeNull(); + }); + }); + + // Tests for exported type guard functions + describe("isMemberDetails", () => { + it("should return true for valid MemberDetails", () => { + const validData = { + userGuid: "user-1", + userName: "John Doe", + email: "john@example.com", + cipherId: "cipher-1", + }; + expect(isMemberDetails(validData)).toBe(true); + }); + + it("should return false for empty userGuid", () => { + const invalidData = { + userGuid: "", + userName: "John Doe", + email: "john@example.com", + cipherId: "cipher-1", + }; + expect(isMemberDetails(invalidData)).toBe(false); + }); + + it("should return false for empty userName", () => { + const invalidData = { + userGuid: "user-1", + userName: "", + email: "john@example.com", + cipherId: "cipher-1", + }; + expect(isMemberDetails(invalidData)).toBe(false); + }); + + it("should return false for empty email", () => { + const invalidData = { + userGuid: "user-1", + userName: "John Doe", + email: "", + cipherId: "cipher-1", + }; + expect(isMemberDetails(invalidData)).toBe(false); + }); + + it("should return false for empty cipherId", () => { + const invalidData = { + userGuid: "user-1", + userName: "John Doe", + email: "john@example.com", + cipherId: "", + }; + expect(isMemberDetails(invalidData)).toBe(false); + }); + + it("should return false for objects with unexpected properties", () => { + const invalidData = { + userGuid: "user-1", + userName: "John Doe", + email: "john@example.com", + cipherId: "cipher-1", + unexpectedProperty: "should fail", + }; + expect(isMemberDetails(invalidData)).toBe(false); + }); + + it("should return false for prototype pollution attempts", () => { + const invalidData = { + userGuid: "user-1", + userName: "John Doe", + email: "john@example.com", + cipherId: "cipher-1", + __proto__: { malicious: "payload" }, + }; + expect(isMemberDetails(invalidData)).toBe(false); + }); + }); + + describe("isApplicationHealthReportDetail", () => { + it("should return true for valid ApplicationHealthReportDetail", () => { + const validData = { + applicationName: "Test App", + passwordCount: 10, + atRiskPasswordCount: 2, + atRiskCipherIds: ["cipher-1"], + memberCount: 5, + atRiskMemberCount: 1, + memberDetails: [] as MemberDetails[], + atRiskMemberDetails: [] as MemberDetails[], + cipherIds: ["cipher-1"], + }; + expect(isApplicationHealthReportDetail(validData)).toBe(true); + }); + + it("should return false for empty applicationName", () => { + const invalidData = { + applicationName: "", + passwordCount: 10, + atRiskPasswordCount: 2, + atRiskCipherIds: ["cipher-1"], + memberCount: 5, + atRiskMemberCount: 1, + memberDetails: [] as MemberDetails[], + atRiskMemberDetails: [] as MemberDetails[], + cipherIds: ["cipher-1"], + }; + expect(isApplicationHealthReportDetail(invalidData)).toBe(false); + }); + + it("should return false for NaN passwordCount", () => { + const invalidData = { + applicationName: "Test App", + passwordCount: NaN, + atRiskPasswordCount: 2, + atRiskCipherIds: ["cipher-1"], + memberCount: 5, + atRiskMemberCount: 1, + memberDetails: [] as MemberDetails[], + atRiskMemberDetails: [] as MemberDetails[], + cipherIds: ["cipher-1"], + }; + expect(isApplicationHealthReportDetail(invalidData)).toBe(false); + }); + + it("should return false for Infinity passwordCount", () => { + const invalidData = { + applicationName: "Test App", + passwordCount: Infinity, + atRiskPasswordCount: 2, + atRiskCipherIds: ["cipher-1"], + memberCount: 5, + atRiskMemberCount: 1, + memberDetails: [] as MemberDetails[], + atRiskMemberDetails: [] as MemberDetails[], + cipherIds: ["cipher-1"], + }; + expect(isApplicationHealthReportDetail(invalidData)).toBe(false); + }); + + it("should return false for negative passwordCount", () => { + const invalidData = { + applicationName: "Test App", + passwordCount: -5, + atRiskPasswordCount: 2, + atRiskCipherIds: ["cipher-1"], + memberCount: 5, + atRiskMemberCount: 1, + memberDetails: [] as MemberDetails[], + atRiskMemberDetails: [] as MemberDetails[], + cipherIds: ["cipher-1"], + }; + expect(isApplicationHealthReportDetail(invalidData)).toBe(false); + }); + + it("should return false for negative memberCount", () => { + const invalidData = { + applicationName: "Test App", + passwordCount: 10, + atRiskPasswordCount: 2, + atRiskCipherIds: ["cipher-1"], + memberCount: -1, + atRiskMemberCount: 1, + memberDetails: [] as MemberDetails[], + atRiskMemberDetails: [] as MemberDetails[], + cipherIds: ["cipher-1"], + }; + expect(isApplicationHealthReportDetail(invalidData)).toBe(false); + }); + + it("should return false for objects with unexpected properties", () => { + const invalidData = { + applicationName: "Test App", + passwordCount: 10, + atRiskPasswordCount: 2, + atRiskCipherIds: ["cipher-1"], + memberCount: 5, + atRiskMemberCount: 1, + memberDetails: [] as MemberDetails[], + atRiskMemberDetails: [] as MemberDetails[], + cipherIds: ["cipher-1"], + injectedProperty: "malicious", + }; + expect(isApplicationHealthReportDetail(invalidData)).toBe(false); + }); + }); + + describe("isOrganizationReportSummary", () => { + it("should return true for valid OrganizationReportSummary", () => { + const validData = { + totalMemberCount: 10, + totalApplicationCount: 5, + totalAtRiskMemberCount: 2, + totalAtRiskApplicationCount: 1, + totalCriticalApplicationCount: 3, + totalCriticalMemberCount: 4, + totalCriticalAtRiskMemberCount: 1, + totalCriticalAtRiskApplicationCount: 1, + newApplications: ["app-1"], + }; + expect(isOrganizationReportSummary(validData)).toBe(true); + }); + + it("should return false for NaN totalMemberCount", () => { + const invalidData = { + totalMemberCount: NaN, + totalApplicationCount: 5, + totalAtRiskMemberCount: 2, + totalAtRiskApplicationCount: 1, + totalCriticalApplicationCount: 3, + totalCriticalMemberCount: 4, + totalCriticalAtRiskMemberCount: 1, + totalCriticalAtRiskApplicationCount: 1, + newApplications: ["app-1"], + }; + expect(isOrganizationReportSummary(invalidData)).toBe(false); + }); + + it("should return false for Infinity totalApplicationCount", () => { + const invalidData = { + totalMemberCount: 10, + totalApplicationCount: Infinity, + totalAtRiskMemberCount: 2, + totalAtRiskApplicationCount: 1, + totalCriticalApplicationCount: 3, + totalCriticalMemberCount: 4, + totalCriticalAtRiskMemberCount: 1, + totalCriticalAtRiskApplicationCount: 1, + newApplications: ["app-1"], + }; + expect(isOrganizationReportSummary(invalidData)).toBe(false); + }); + + it("should return false for negative totalAtRiskMemberCount", () => { + const invalidData = { + totalMemberCount: 10, + totalApplicationCount: 5, + totalAtRiskMemberCount: -1, + totalAtRiskApplicationCount: 1, + totalCriticalApplicationCount: 3, + totalCriticalMemberCount: 4, + totalCriticalAtRiskMemberCount: 1, + totalCriticalAtRiskApplicationCount: 1, + newApplications: ["app-1"], + }; + expect(isOrganizationReportSummary(invalidData)).toBe(false); + }); + + it("should return false for objects with unexpected properties", () => { + const invalidData = { + totalMemberCount: 10, + totalApplicationCount: 5, + totalAtRiskMemberCount: 2, + totalAtRiskApplicationCount: 1, + totalCriticalApplicationCount: 3, + totalCriticalMemberCount: 4, + totalCriticalAtRiskMemberCount: 1, + totalCriticalAtRiskApplicationCount: 1, + newApplications: ["app-1"], + extraField: "should be rejected", + }; + expect(isOrganizationReportSummary(invalidData)).toBe(false); + }); + }); + + describe("isOrganizationReportApplication", () => { + it("should return true for valid OrganizationReportApplication", () => { + const validData = { + applicationName: "Test App", + isCritical: true, + reviewedDate: null as Date | null, + }; + expect(isOrganizationReportApplication(validData)).toBe(true); + }); + + it("should return false for empty applicationName", () => { + const invalidData = { + applicationName: "", + isCritical: true, + reviewedDate: null as Date | null, + }; + expect(isOrganizationReportApplication(invalidData)).toBe(false); + }); + + it("should return true for Date reviewedDate", () => { + const validData = { + applicationName: "Test App", + isCritical: true, + reviewedDate: new Date(), + }; + expect(isOrganizationReportApplication(validData)).toBe(true); + }); + + it("should return true for string reviewedDate", () => { + const validData = { + applicationName: "Test App", + isCritical: false, + reviewedDate: "2024-01-01", + }; + expect(isOrganizationReportApplication(validData)).toBe(true); + }); + + it("should return false for objects with unexpected properties", () => { + const invalidData = { + applicationName: "Test App", + isCritical: true, + reviewedDate: null as Date | null, + injectedProperty: "malicious", + }; + expect(isOrganizationReportApplication(invalidData)).toBe(false); + }); + + it("should return false for prototype pollution attempts via __proto__", () => { + const invalidData = { + applicationName: "Test App", + isCritical: true, + reviewedDate: null as Date | null, + __proto__: { polluted: true }, + }; + expect(isOrganizationReportApplication(invalidData)).toBe(false); + }); + }); +}); diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-type-guards.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-type-guards.ts new file mode 100644 index 00000000000..b1d2550d4fa --- /dev/null +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/domain/risk-insights-type-guards.ts @@ -0,0 +1,404 @@ +import { + ApplicationHealthReportDetail, + MemberDetails, + OrganizationReportApplication, + OrganizationReportSummary, +} from "../../models"; + +/** + * Security limits for validation (prevent DoS attacks and ensure reasonable data sizes) + */ +const MAX_STRING_LENGTH = 1000; // Reasonable limit for names, emails, GUIDs +const MAX_ARRAY_LENGTH = 50000; // Reasonable limit for report arrays +const MAX_COUNT = 10000000; // 10 million - reasonable upper bound for count fields + +/** + * Type guard to validate MemberDetails structure + * Exported for testability + * Strict validation: rejects objects with unexpected properties and prototype pollution + */ +export function isMemberDetails(obj: any): obj is MemberDetails { + if (typeof obj !== "object" || obj === null) { + return false; + } + + // Prevent prototype pollution - check prototype is Object.prototype + if (Object.getPrototypeOf(obj) !== Object.prototype) { + return false; + } + + // Prevent dangerous properties that could be used for prototype pollution + // Check for __proto__, constructor, and prototype as own properties + const dangerousKeys = ["__proto__", "constructor", "prototype"]; + for (const key of dangerousKeys) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + return false; + } + } + + // Strict property validation - reject unexpected properties + const allowedKeys = ["userGuid", "userName", "email", "cipherId"]; + const actualKeys = Object.keys(obj); + const hasUnexpectedProps = actualKeys.some((key) => !allowedKeys.includes(key)); + if (hasUnexpectedProps) { + return false; + } + + return ( + typeof obj.userGuid === "string" && + obj.userGuid.length > 0 && + obj.userGuid.length <= MAX_STRING_LENGTH && + typeof obj.userName === "string" && + obj.userName.length > 0 && + obj.userName.length <= MAX_STRING_LENGTH && + typeof obj.email === "string" && + obj.email.length > 0 && + obj.email.length <= MAX_STRING_LENGTH && + typeof obj.cipherId === "string" && + obj.cipherId.length > 0 && + obj.cipherId.length <= MAX_STRING_LENGTH + ); +} + +/** + * Type guard to validate ApplicationHealthReportDetail structure + * Exported for testability + * Strict validation: rejects objects with unexpected properties and prototype pollution + */ +export function isApplicationHealthReportDetail(obj: any): obj is ApplicationHealthReportDetail { + if (typeof obj !== "object" || obj === null) { + return false; + } + + // Prevent prototype pollution - check prototype is Object.prototype + if (Object.getPrototypeOf(obj) !== Object.prototype) { + return false; + } + + // Prevent dangerous properties that could be used for prototype pollution + // Check for __proto__, constructor, and prototype as own properties + const dangerousKeys = ["__proto__", "constructor", "prototype"]; + for (const key of dangerousKeys) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + return false; + } + } + + // Strict property validation - reject unexpected properties + const allowedKeys = [ + "applicationName", + "passwordCount", + "atRiskPasswordCount", + "atRiskCipherIds", + "memberCount", + "atRiskMemberCount", + "memberDetails", + "atRiskMemberDetails", + "cipherIds", + ]; + const actualKeys = Object.keys(obj); + const hasUnexpectedProps = actualKeys.some((key) => !allowedKeys.includes(key)); + if (hasUnexpectedProps) { + return false; + } + + return ( + typeof obj.applicationName === "string" && + obj.applicationName.length > 0 && + obj.applicationName.length <= MAX_STRING_LENGTH && + typeof obj.passwordCount === "number" && + Number.isFinite(obj.passwordCount) && + Number.isSafeInteger(obj.passwordCount) && + obj.passwordCount >= 0 && + obj.passwordCount <= MAX_COUNT && + typeof obj.atRiskPasswordCount === "number" && + Number.isFinite(obj.atRiskPasswordCount) && + Number.isSafeInteger(obj.atRiskPasswordCount) && + obj.atRiskPasswordCount >= 0 && + obj.atRiskPasswordCount <= MAX_COUNT && + Array.isArray(obj.atRiskCipherIds) && + obj.atRiskCipherIds.length <= MAX_ARRAY_LENGTH && + obj.atRiskCipherIds.every( + (id: any) => typeof id === "string" && id.length > 0 && id.length <= MAX_STRING_LENGTH, + ) && + typeof obj.memberCount === "number" && + Number.isFinite(obj.memberCount) && + Number.isSafeInteger(obj.memberCount) && + obj.memberCount >= 0 && + obj.memberCount <= MAX_COUNT && + typeof obj.atRiskMemberCount === "number" && + Number.isFinite(obj.atRiskMemberCount) && + Number.isSafeInteger(obj.atRiskMemberCount) && + obj.atRiskMemberCount >= 0 && + obj.atRiskMemberCount <= MAX_COUNT && + Array.isArray(obj.memberDetails) && + obj.memberDetails.length <= MAX_ARRAY_LENGTH && + obj.memberDetails.every(isMemberDetails) && + Array.isArray(obj.atRiskMemberDetails) && + obj.atRiskMemberDetails.length <= MAX_ARRAY_LENGTH && + obj.atRiskMemberDetails.every(isMemberDetails) && + Array.isArray(obj.cipherIds) && + obj.cipherIds.length <= MAX_ARRAY_LENGTH && + obj.cipherIds.every( + (id: any) => typeof id === "string" && id.length > 0 && id.length <= MAX_STRING_LENGTH, + ) + ); +} + +/** + * Type guard to validate OrganizationReportSummary structure + * Exported for testability + * Strict validation: rejects objects with unexpected properties and prototype pollution + */ +export function isOrganizationReportSummary(obj: any): obj is OrganizationReportSummary { + if (typeof obj !== "object" || obj === null) { + return false; + } + + // Prevent prototype pollution - check prototype is Object.prototype + if (Object.getPrototypeOf(obj) !== Object.prototype) { + return false; + } + + // Prevent dangerous properties that could be used for prototype pollution + // Check for __proto__, constructor, and prototype as own properties + const dangerousKeys = ["__proto__", "constructor", "prototype"]; + for (const key of dangerousKeys) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + return false; + } + } + + // Strict property validation - reject unexpected properties + const allowedKeys = [ + "totalMemberCount", + "totalApplicationCount", + "totalAtRiskMemberCount", + "totalAtRiskApplicationCount", + "totalCriticalApplicationCount", + "totalCriticalMemberCount", + "totalCriticalAtRiskMemberCount", + "totalCriticalAtRiskApplicationCount", + "newApplications", + ]; + const actualKeys = Object.keys(obj); + const hasUnexpectedProps = actualKeys.some((key) => !allowedKeys.includes(key)); + if (hasUnexpectedProps) { + return false; + } + + return ( + typeof obj.totalMemberCount === "number" && + Number.isFinite(obj.totalMemberCount) && + Number.isSafeInteger(obj.totalMemberCount) && + obj.totalMemberCount >= 0 && + obj.totalMemberCount <= MAX_COUNT && + typeof obj.totalApplicationCount === "number" && + Number.isFinite(obj.totalApplicationCount) && + Number.isSafeInteger(obj.totalApplicationCount) && + obj.totalApplicationCount >= 0 && + obj.totalApplicationCount <= MAX_COUNT && + typeof obj.totalAtRiskMemberCount === "number" && + Number.isFinite(obj.totalAtRiskMemberCount) && + Number.isSafeInteger(obj.totalAtRiskMemberCount) && + obj.totalAtRiskMemberCount >= 0 && + obj.totalAtRiskMemberCount <= MAX_COUNT && + typeof obj.totalAtRiskApplicationCount === "number" && + Number.isFinite(obj.totalAtRiskApplicationCount) && + Number.isSafeInteger(obj.totalAtRiskApplicationCount) && + obj.totalAtRiskApplicationCount >= 0 && + obj.totalAtRiskApplicationCount <= MAX_COUNT && + typeof obj.totalCriticalApplicationCount === "number" && + Number.isFinite(obj.totalCriticalApplicationCount) && + Number.isSafeInteger(obj.totalCriticalApplicationCount) && + obj.totalCriticalApplicationCount >= 0 && + obj.totalCriticalApplicationCount <= MAX_COUNT && + typeof obj.totalCriticalMemberCount === "number" && + Number.isFinite(obj.totalCriticalMemberCount) && + Number.isSafeInteger(obj.totalCriticalMemberCount) && + obj.totalCriticalMemberCount >= 0 && + obj.totalCriticalMemberCount <= MAX_COUNT && + typeof obj.totalCriticalAtRiskMemberCount === "number" && + Number.isFinite(obj.totalCriticalAtRiskMemberCount) && + Number.isSafeInteger(obj.totalCriticalAtRiskMemberCount) && + obj.totalCriticalAtRiskMemberCount >= 0 && + obj.totalCriticalAtRiskMemberCount <= MAX_COUNT && + typeof obj.totalCriticalAtRiskApplicationCount === "number" && + Number.isFinite(obj.totalCriticalAtRiskApplicationCount) && + Number.isSafeInteger(obj.totalCriticalAtRiskApplicationCount) && + obj.totalCriticalAtRiskApplicationCount >= 0 && + obj.totalCriticalAtRiskApplicationCount <= MAX_COUNT && + Array.isArray(obj.newApplications) && + obj.newApplications.length <= MAX_ARRAY_LENGTH && + obj.newApplications.every( + (app: any) => typeof app === "string" && app.length > 0 && app.length <= MAX_STRING_LENGTH, + ) + ); +} + +/** + * Type guard to validate OrganizationReportApplication structure + * Exported for testability + * Strict validation: rejects objects with unexpected properties and prototype pollution + */ +export function isOrganizationReportApplication(obj: any): obj is OrganizationReportApplication { + if (typeof obj !== "object" || obj === null) { + return false; + } + + // Prevent prototype pollution - check prototype is Object.prototype + if (Object.getPrototypeOf(obj) !== Object.prototype) { + return false; + } + + // Prevent dangerous properties that could be used for prototype pollution + // Check for __proto__, constructor, and prototype as own properties + const dangerousKeys = ["__proto__", "constructor", "prototype"]; + for (const key of dangerousKeys) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + return false; + } + } + + // Strict property validation - reject unexpected properties + const allowedKeys = ["applicationName", "isCritical", "reviewedDate"]; + const actualKeys = Object.keys(obj); + const hasUnexpectedProps = actualKeys.some((key) => !allowedKeys.includes(key)); + if (hasUnexpectedProps) { + return false; + } + + return ( + typeof obj.applicationName === "string" && + obj.applicationName.length > 0 && + obj.applicationName.length <= MAX_STRING_LENGTH && + typeof obj.isCritical === "boolean" && + (obj.reviewedDate === null || + obj.reviewedDate instanceof Date || + typeof obj.reviewedDate === "string") + ); +} + +/** + * Validates and returns an array of ApplicationHealthReportDetail + * @throws Error if validation fails + */ +export function validateApplicationHealthReportDetailArray( + data: any, +): ApplicationHealthReportDetail[] { + if (!Array.isArray(data)) { + throw new Error( + "Invalid report data: expected array of ApplicationHealthReportDetail, received non-array", + ); + } + + if (data.length > MAX_ARRAY_LENGTH) { + throw new Error( + `Invalid report data: array length ${data.length} exceeds maximum allowed length ${MAX_ARRAY_LENGTH}`, + ); + } + + const invalidItems = data + .map((item, index) => ({ item, index })) + .filter(({ item }) => !isApplicationHealthReportDetail(item)); + + if (invalidItems.length > 0) { + const invalidIndices = invalidItems.map(({ index }) => index).join(", "); + throw new Error( + `Invalid report data: array contains ${invalidItems.length} invalid ApplicationHealthReportDetail element(s) at indices: ${invalidIndices}`, + ); + } + + return data as ApplicationHealthReportDetail[]; +} + +/** + * Validates and returns OrganizationReportSummary + * @throws Error if validation fails + */ +export function validateOrganizationReportSummary(data: any): OrganizationReportSummary { + if (!isOrganizationReportSummary(data)) { + const missingFields: string[] = []; + + if (typeof data?.totalMemberCount !== "number") { + missingFields.push("totalMemberCount (number)"); + } + if (typeof data?.totalApplicationCount !== "number") { + missingFields.push("totalApplicationCount (number)"); + } + if (typeof data?.totalAtRiskMemberCount !== "number") { + missingFields.push("totalAtRiskMemberCount (number)"); + } + if (typeof data?.totalAtRiskApplicationCount !== "number") { + missingFields.push("totalAtRiskApplicationCount (number)"); + } + if (typeof data?.totalCriticalApplicationCount !== "number") { + missingFields.push("totalCriticalApplicationCount (number)"); + } + if (typeof data?.totalCriticalMemberCount !== "number") { + missingFields.push("totalCriticalMemberCount (number)"); + } + if (typeof data?.totalCriticalAtRiskMemberCount !== "number") { + missingFields.push("totalCriticalAtRiskMemberCount (number)"); + } + if (typeof data?.totalCriticalAtRiskApplicationCount !== "number") { + missingFields.push("totalCriticalAtRiskApplicationCount (number)"); + } + if (!Array.isArray(data?.newApplications)) { + missingFields.push("newApplications (string[])"); + } + + throw new Error( + `Invalid OrganizationReportSummary: ${missingFields.length > 0 ? `missing or invalid fields: ${missingFields.join(", ")}` : "structure validation failed"}`, + ); + } + + return data as OrganizationReportSummary; +} + +/** + * Validates and returns an array of OrganizationReportApplication + * @throws Error if validation fails + */ +export function validateOrganizationReportApplicationArray( + data: any, +): OrganizationReportApplication[] { + if (!Array.isArray(data)) { + throw new Error( + "Invalid application data: expected array of OrganizationReportApplication, received non-array", + ); + } + + if (data.length > MAX_ARRAY_LENGTH) { + throw new Error( + `Invalid application data: array length ${data.length} exceeds maximum allowed length ${MAX_ARRAY_LENGTH}`, + ); + } + + const invalidItems = data + .map((item, index) => ({ item, index })) + .filter(({ item }) => !isOrganizationReportApplication(item)); + + if (invalidItems.length > 0) { + const invalidIndices = invalidItems.map(({ index }) => index).join(", "); + throw new Error( + `Invalid application data: array contains ${invalidItems.length} invalid OrganizationReportApplication element(s) at indices: ${invalidIndices}`, + ); + } + + // Convert string dates to Date objects for reviewedDate + return data.map((item) => ({ + ...item, + reviewedDate: item.reviewedDate + ? item.reviewedDate instanceof Date + ? item.reviewedDate + : (() => { + const date = new Date(item.reviewedDate); + if (isNaN(date.getTime())) { + throw new Error(`Invalid date string: ${item.reviewedDate}`); + } + return date; + })() + : null, + })) as OrganizationReportApplication[]; +} From 714daa57797bb2c8cf2e01d28504ebe05abdddc3 Mon Sep 17 00:00:00 2001 From: Miles Blackwood <mrobinson@bitwarden.com> Date: Tue, 28 Oct 2025 11:09:29 -0400 Subject: [PATCH 63/73] Removes deprecated keypress event. (#17058) --- .../services/insert-autofill-content.service.spec.ts | 5 ++--- .../src/autofill/services/insert-autofill-content.service.ts | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts b/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts index 07fdfb9db79..63cd4b534fb 100644 --- a/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts +++ b/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts @@ -26,7 +26,6 @@ const eventsToTest = [ EVENTS.CHANGE, EVENTS.INPUT, EVENTS.KEYDOWN, - EVENTS.KEYPRESS, EVENTS.KEYUP, "blur", "click", @@ -1044,13 +1043,13 @@ describe("InsertAutofillContentService", () => { }); describe("simulateUserKeyboardEventInteractions", () => { - it("will trigger `keydown`, `keypress`, and `keyup` events on the passed element", () => { + it("will trigger `keydown` and `keyup` events on the passed element", () => { const inputElement = document.querySelector('input[type="text"]') as HTMLInputElement; jest.spyOn(inputElement, "dispatchEvent"); insertAutofillContentService["simulateUserKeyboardEventInteractions"](inputElement); - [EVENTS.KEYDOWN, EVENTS.KEYPRESS, EVENTS.KEYUP].forEach((eventName) => { + [EVENTS.KEYDOWN, EVENTS.KEYUP].forEach((eventName) => { expect(inputElement.dispatchEvent).toHaveBeenCalledWith( new KeyboardEvent(eventName, { bubbles: true }), ); diff --git a/apps/browser/src/autofill/services/insert-autofill-content.service.ts b/apps/browser/src/autofill/services/insert-autofill-content.service.ts index a809dadf8ed..6c951afc1a0 100644 --- a/apps/browser/src/autofill/services/insert-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/insert-autofill-content.service.ts @@ -349,7 +349,7 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf * @private */ private simulateUserKeyboardEventInteractions(element: FormFieldElement): void { - const simulatedKeyboardEvents = [EVENTS.KEYDOWN, EVENTS.KEYPRESS, EVENTS.KEYUP]; + const simulatedKeyboardEvents = [EVENTS.KEYDOWN, EVENTS.KEYUP]; for (let index = 0; index < simulatedKeyboardEvents.length; index++) { element.dispatchEvent(new KeyboardEvent(simulatedKeyboardEvents[index], { bubbles: true })); } From bf66b5ac19631f18245bac4c8579d11d735e9c11 Mon Sep 17 00:00:00 2001 From: Stephon Brown <sbrown@livefront.com> Date: Tue, 28 Oct 2025 11:25:07 -0400 Subject: [PATCH 64/73] -[PM-27123] Update Signals and Update Estimated Tax and Credit Logic (#17055) * billing(fix): update signals and update estimated tax and credit logic * fix(billing): update with claude feedback and expose total observable --- .../upgrade-payment.component.html | 2 +- .../upgrade-payment.component.ts | 89 +++++++++---------- .../cart-summary/cart-summary.component.ts | 6 ++ 3 files changed, 48 insertions(+), 49 deletions(-) diff --git a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.html b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.html index 9b007ae7a6b..39a80c99458 100644 --- a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.html +++ b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.html @@ -54,7 +54,7 @@ <billing-cart-summary #cartSummaryComponent [passwordManager]="passwordManager" - [estimatedTax]="estimatedTax" + [estimatedTax]="estimatedTax$ | async" ></billing-cart-summary> @if (isFamiliesPlan) { <p bitTypography="helper" class="tw-italic tw-text-muted !tw-mb-0"> diff --git a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.ts b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.ts index f168672f23f..a0ba480fe1e 100644 --- a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.ts +++ b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.ts @@ -1,12 +1,12 @@ import { - AfterViewChecked, + AfterViewInit, Component, DestroyRef, input, OnInit, output, signal, - ViewChild, + viewChild, } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormControl, FormGroup, Validators } from "@angular/forms"; @@ -19,6 +19,8 @@ import { catchError, of, combineLatest, + map, + shareReplay, } from "rxjs"; import { Account } from "@bitwarden/common/auth/abstractions/account.service"; @@ -96,7 +98,8 @@ export type UpgradePaymentParams = { providers: [UpgradePaymentService], templateUrl: "./upgrade-payment.component.html", }) -export class UpgradePaymentComponent implements OnInit, AfterViewChecked { +export class UpgradePaymentComponent implements OnInit, AfterViewInit { + private readonly INITIAL_TAX_VALUE = 0; protected readonly selectedPlanId = input.required<PersonalSubscriptionPricingTierId>(); protected readonly account = input.required<Account>(); protected goBack = output<void>(); @@ -104,12 +107,8 @@ export class UpgradePaymentComponent implements OnInit, AfterViewChecked { protected selectedPlan: PlanDetails | null = null; protected hasEnoughAccountCredit$!: Observable<boolean>; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @ViewChild(EnterPaymentMethodComponent) paymentComponent!: EnterPaymentMethodComponent; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @ViewChild(CartSummaryComponent) cartSummaryComponent!: CartSummaryComponent; + readonly paymentComponent = viewChild.required(EnterPaymentMethodComponent); + readonly cartSummaryComponent = viewChild.required(CartSummaryComponent); protected formGroup = new FormGroup({ organizationName: new FormControl<string>("", [Validators.required]), @@ -118,12 +117,11 @@ export class UpgradePaymentComponent implements OnInit, AfterViewChecked { }); protected readonly loading = signal(true); - private cartSummaryConfigured = false; private pricingTiers$!: Observable<PersonalSubscriptionPricingTier[]>; // Cart Summary data protected passwordManager!: LineItem; - protected estimatedTax = 0; + protected estimatedTax$!: Observable<number>; // Display data protected upgradeToMessage = ""; @@ -165,49 +163,42 @@ export class UpgradePaymentComponent implements OnInit, AfterViewChecked { this.upgradeToMessage = this.i18nService.t( this.isFamiliesPlan ? "upgradeToFamilies" : "upgradeToPremium", ); - - this.estimatedTax = 0; } else { this.complete.emit({ status: UpgradePaymentStatus.Closed, organizationId: null }); return; } }); - this.formGroup.controls.billingAddress.valueChanges - .pipe( - debounceTime(1000), - // Only proceed when form has required values - switchMap(() => this.refreshSalesTax$()), - takeUntilDestroyed(this.destroyRef), - ) - .subscribe((tax) => { - this.estimatedTax = tax; - }); - - // Check if user has enough account credit for the purchase - this.hasEnoughAccountCredit$ = combineLatest([ - this.upgradePaymentService.accountCredit$, - this.formGroup.valueChanges.pipe(startWith(this.formGroup.value)), - ]).pipe( - switchMap(([credit, formValue]) => { - const selectedPaymentType = formValue.paymentForm?.type; - if (selectedPaymentType !== NonTokenizablePaymentMethods.accountCredit) { - return of(true); // Not using account credit, so this check doesn't apply - } - - return credit ? of(credit >= this.cartSummaryComponent.total()) : of(false); - }), + this.estimatedTax$ = this.formGroup.controls.billingAddress.valueChanges.pipe( + startWith(this.formGroup.controls.billingAddress.value), + debounceTime(1000), + // Only proceed when form has required values + switchMap(() => this.refreshSalesTax$()), ); this.loading.set(false); } - ngAfterViewChecked(): void { - // Configure cart summary only once when it becomes available - if (this.cartSummaryComponent && !this.cartSummaryConfigured) { - this.cartSummaryComponent.isExpanded.set(false); - this.cartSummaryConfigured = true; - } + ngAfterViewInit(): void { + const cartSummaryComponent = this.cartSummaryComponent(); + cartSummaryComponent.isExpanded.set(false); + + this.hasEnoughAccountCredit$ = combineLatest([ + cartSummaryComponent.total$, + this.upgradePaymentService.accountCredit$, + this.formGroup.controls.paymentForm.valueChanges.pipe( + startWith(this.formGroup.controls.paymentForm.value), + ), + ]).pipe( + map(([total, credit, currentFormValue]) => { + const selectedPaymentType = currentFormValue?.type; + if (selectedPaymentType !== NonTokenizablePaymentMethods.accountCredit) { + return true; // Not using account credit, so this check doesn't apply + } + return credit ? credit >= total : false; + }), + shareReplay({ bufferSize: 1, refCount: true }), // Cache the latest for two async pipes + ); } protected get isPremiumPlan(): boolean { @@ -252,7 +243,7 @@ export class UpgradePaymentComponent implements OnInit, AfterViewChecked { }; protected isFormValid(): boolean { - return this.formGroup.valid && this.paymentComponent?.validate(); + return this.formGroup.valid && this.paymentComponent().validate(); } private async processUpgrade(): Promise<UpgradePaymentResult> { @@ -335,17 +326,19 @@ export class UpgradePaymentComponent implements OnInit, AfterViewChecked { return { type: NonTokenizablePaymentMethods.accountCredit }; } - return await this.paymentComponent?.tokenize(); + return await this.paymentComponent().tokenize(); } // Create an observable for tax calculation private refreshSalesTax$(): Observable<number> { if (this.formGroup.invalid || !this.selectedPlan) { - return of(0); + return of(this.INITIAL_TAX_VALUE); } const billingAddress = getBillingAddressFromForm(this.formGroup.controls.billingAddress); - + if (!billingAddress.country || !billingAddress.postalCode) { + return of(this.INITIAL_TAX_VALUE); + } return from( this.upgradePaymentService.calculateEstimatedTax(this.selectedPlan, billingAddress), ).pipe( @@ -355,7 +348,7 @@ export class UpgradePaymentComponent implements OnInit, AfterViewChecked { variant: "error", message: this.i18nService.t("taxCalculationError"), }); - return of(0); // Return default value on error + return of(this.INITIAL_TAX_VALUE); // Return default value on error }), ); } diff --git a/libs/pricing/src/components/cart-summary/cart-summary.component.ts b/libs/pricing/src/components/cart-summary/cart-summary.component.ts index 11c6cddcab1..5f1da4a1cd8 100644 --- a/libs/pricing/src/components/cart-summary/cart-summary.component.ts +++ b/libs/pricing/src/components/cart-summary/cart-summary.component.ts @@ -1,5 +1,6 @@ import { CurrencyPipe } from "@angular/common"; import { Component, computed, input, signal } from "@angular/core"; +import { toObservable } from "@angular/core/rxjs-interop"; import { TypographyModule, IconButtonModule } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; @@ -71,6 +72,11 @@ export class CartSummaryComponent { */ readonly total = computed<number>(() => this.getTotalCost()); + /** + * Observable of computed total value + */ + readonly total$ = toObservable(this.total); + /** * Toggles the expanded/collapsed state of the cart items */ From 2058c772ac9fc6aee0cc3c28d7995c84525075d1 Mon Sep 17 00:00:00 2001 From: Alex <55413326+AlexRubik@users.noreply.github.com> Date: Tue, 28 Oct 2025 11:44:42 -0400 Subject: [PATCH 65/73] [PM-26352] drawers for activity cards (#16895) * new drawer functions for crit apps * logic for triggering the drawer functions in components * cleanup unused logic and rename "navigation" to "action" - ... since the click is now triggering the drawer instead of navigating to another tab/page * null check for reportData in drawer methods * use criticalReportResults$ to avoid duplicating logic * use criticalReportResults$ to avoid dupe logic * remove unused code --- .../view/risk-insights-data.service.ts | 59 +++++++++++++++++++ .../activity/activity-card.component.html | 6 +- .../activity/activity-card.component.ts | 32 ++++------ .../activity/all-activity.component.html | 12 ++-- .../activity/all-activity.component.ts | 26 ++++---- 5 files changed, 97 insertions(+), 38 deletions(-) diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/view/risk-insights-data.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/view/risk-insights-data.service.ts index 89f120cbded..6855274498a 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/view/risk-insights-data.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/view/risk-insights-data.service.ts @@ -175,6 +175,65 @@ export class RiskInsightsDataService { } }; + setDrawerForCriticalAtRiskMembers = async (invokerId: string = ""): Promise<void> => { + const { open, activeDrawerType, invokerId: currentInvokerId } = this.drawerDetailsSubject.value; + const shouldClose = + open && activeDrawerType === DrawerType.OrgAtRiskMembers && currentInvokerId === invokerId; + + if (shouldClose) { + this.closeDrawer(); + } else { + const reportResults = await firstValueFrom(this.criticalReportResults$); + if (!reportResults?.reportData) { + return; + } + + // Generate at-risk member list from critical applications + const atRiskMemberDetails = getAtRiskMemberList(reportResults.reportData); + + this.drawerDetailsSubject.next({ + open: true, + invokerId, + activeDrawerType: DrawerType.OrgAtRiskMembers, + atRiskMemberDetails, + appAtRiskMembers: null, + atRiskAppDetails: null, + }); + } + }; + + setDrawerForCriticalAtRiskApps = async (invokerId: string = ""): Promise<void> => { + const { open, activeDrawerType, invokerId: currentInvokerId } = this.drawerDetailsSubject.value; + const shouldClose = + open && activeDrawerType === DrawerType.OrgAtRiskApps && currentInvokerId === invokerId; + + if (shouldClose) { + this.closeDrawer(); + } else { + const reportResults = await firstValueFrom(this.criticalReportResults$); + if (!reportResults?.reportData) { + return; + } + + // Filter critical applications for those with at-risk passwords + const criticalAtRiskApps = reportResults.reportData + .filter((app) => app.atRiskPasswordCount > 0) + .map((app) => ({ + applicationName: app.applicationName, + atRiskPasswordCount: app.atRiskPasswordCount, + })); + + this.drawerDetailsSubject.next({ + open: true, + invokerId, + activeDrawerType: DrawerType.OrgAtRiskApps, + atRiskMemberDetails: [], + appAtRiskMembers: null, + atRiskAppDetails: criticalAtRiskApps, + }); + } + }; + // ------------------------------ Critical application methods -------------- saveCriticalApplications(selectedUrls: string[]) { return this.orchestrator.saveCriticalApplications$(selectedUrls); diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.html index 0eb9b30367c..756907d24e6 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.html @@ -23,11 +23,11 @@ </button> </div> } - @if (showNavigationLink && !buttonText) { + @if (showActionLink && !buttonText) { <div class="tw-flex tw-items-baseline tw-mt-4 tw-gap-2"> <p bitTypography="body1"> - <a bitLink (click)="navigateToLink(navigationLink)" rel="noreferrer"> - {{ navigationText }} + <a bitLink href="#" (click)="onActionClick(); $event.preventDefault()" rel="noreferrer"> + {{ actionText }} </a> </p> </div> diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.ts index 7abedb06a7c..84c763841b5 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-card.component.ts @@ -37,25 +37,14 @@ export class ActivityCardComponent { @Input() metricDescription: string = ""; /** - * The link to navigate to for more information + * The text to display for the action link */ - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() navigationLink: string = ""; + @Input() actionText: string = ""; /** - * The text to display for the navigation link + * Show action link */ - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() navigationText: string = ""; - - /** - * Show Navigation link - */ - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() showNavigationLink: boolean = false; + @Input() showActionLink: boolean = false; /** * Icon class to display next to metrics (e.g., "bwi-exclamation-triangle"). @@ -86,13 +75,18 @@ export class ActivityCardComponent { // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() buttonClick = new EventEmitter<void>(); - constructor(private router: Router) {} + /** + * Event emitted when action link is clicked + */ + @Output() actionClick = new EventEmitter<void>(); - navigateToLink = async (navigationLink: string) => { - await this.router.navigateByUrl(navigationLink); - }; + constructor(private router: Router) {} onButtonClick = () => { this.buttonClick.emit(); }; + + onActionClick = () => { + this.actionClick.emit(); + }; } diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/all-activity.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/all-activity.component.html index 844b2f92bb3..9fffded215e 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/all-activity.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/all-activity.component.html @@ -13,9 +13,9 @@ [title]="'atRiskMembers' | i18n" [cardMetrics]="'membersAtRiskCount' | i18n: totalCriticalAppsAtRiskMemberCount" [metricDescription]="'membersWithAccessToAtRiskItemsForCriticalApps' | i18n" - navigationText="{{ 'viewAtRiskMembers' | i18n }}" - navigationLink="{{ getLinkForRiskInsightsTab(RiskInsightsTabType.AllApps) }}" - [showNavigationLink]="totalCriticalAppsAtRiskMemberCount > 0" + actionText="{{ 'viewAtRiskMembers' | i18n }}" + [showActionLink]="totalCriticalAppsAtRiskMemberCount > 0" + (actionClick)="onViewAtRiskMembers()" > </dirt-activity-card> </li> @@ -35,9 +35,9 @@ : ('criticalApplicationsAreAtRisk' | i18n: totalCriticalAppsAtRiskCount : totalCriticalAppsCount) " - navigationText="{{ 'viewAtRiskApplications' | i18n }}" - navigationLink="{{ getLinkForRiskInsightsTab(RiskInsightsTabType.CriticalApps) }}" - [showNavigationLink]="totalCriticalAppsAtRiskCount > 0" + actionText="{{ 'viewAtRiskApplications' | i18n }}" + [showActionLink]="totalCriticalAppsAtRiskCount > 0" + (actionClick)="onViewAtRiskApplications()" > </dirt-activity-card> </li> diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/all-activity.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/all-activity.component.ts index 947e2f2fa42..9689110866a 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/all-activity.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/all-activity.component.ts @@ -15,7 +15,6 @@ import { getById } from "@bitwarden/common/platform/misc"; import { DialogService } from "@bitwarden/components"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; -import { RiskInsightsTabType } from "../models/risk-insights.models"; import { ApplicationsLoadingComponent } from "../shared/risk-insights-loading.component"; import { ActivityCardComponent } from "./activity-card.component"; @@ -82,15 +81,6 @@ export class AllActivityComponent implements OnInit { } } - get RiskInsightsTabType() { - return RiskInsightsTabType; - } - - getLinkForRiskInsightsTab(tabIndex: RiskInsightsTabType): string { - const organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId"); - return `/organizations/${organizationId}/access-intelligence/risk-insights?tabIndex=${tabIndex}`; - } - /** * Handles the review new applications button click. * Opens a dialog showing the list of new applications that can be marked as critical. @@ -102,4 +92,20 @@ export class AllActivityComponent implements OnInit { await firstValueFrom(dialogRef.closed); }; + + /** + * Handles the "View at-risk members" link click. + * Opens the at-risk members drawer for critical applications only. + */ + onViewAtRiskMembers = async () => { + await this.dataService.setDrawerForCriticalAtRiskMembers("activityTabAtRiskMembers"); + }; + + /** + * Handles the "View at-risk applications" link click. + * Opens the at-risk applications drawer for critical applications only. + */ + onViewAtRiskApplications = async () => { + await this.dataService.setDrawerForCriticalAtRiskApps("activityTabAtRiskApplications"); + }; } From 8d54ad7883e7ac393fc1b6c3247f7b1843265c71 Mon Sep 17 00:00:00 2001 From: John Harrington <84741727+harr1424@users.noreply.github.com> Date: Tue, 28 Oct 2025 09:02:38 -0700 Subject: [PATCH 66/73] PM-26201 [Defect] [Safari] Cannot unzip vault export (#16909) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • ensure extension method can accept both `blob` type and `arrayBuffer` type • replace usage of Swift's `url.absoluteString` with `url.path` • explicitly discard promise returned by `downloadSafari()` • confine `data` type to `string` since code all code paths assign a `string` value --- .../services/browser-file-download.service.ts | 47 ++++++++++++------- .../safari/SafariWebExtensionHandler.swift | 4 +- 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/apps/browser/src/platform/popup/services/browser-file-download.service.ts b/apps/browser/src/platform/popup/services/browser-file-download.service.ts index ec04adac2af..a30c7fe02c8 100644 --- a/apps/browser/src/platform/popup/services/browser-file-download.service.ts +++ b/apps/browser/src/platform/popup/services/browser-file-download.service.ts @@ -15,23 +15,9 @@ export class BrowserFileDownloadService implements FileDownloadService { download(request: FileDownloadRequest): void { const builder = new FileDownloadBuilder(request); if (BrowserApi.isSafariApi) { - let data: BlobPart = null; - if (builder.blobOptions.type === "text/plain" && typeof request.blobData === "string") { - data = request.blobData; - } else { - data = Utils.fromBufferToB64(request.blobData as ArrayBuffer); - } - // 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 - SafariApp.sendMessageToApp( - "downloadFile", - JSON.stringify({ - blobData: data, - blobOptions: request.blobOptions, - fileName: request.fileName, - }), - true, - ); + // Handle Safari download asynchronously to allow Blob conversion + // This function can't be async because the interface is not async + void this.downloadSafari(request, builder); } else { const a = window.document.createElement("a"); a.href = URL.createObjectURL(builder.blob); @@ -41,4 +27,31 @@ export class BrowserFileDownloadService implements FileDownloadService { window.document.body.removeChild(a); } } + + private async downloadSafari( + request: FileDownloadRequest, + builder: FileDownloadBuilder, + ): Promise<void> { + let data: string = null; + if (builder.blobOptions.type === "text/plain" && typeof request.blobData === "string") { + data = request.blobData; + } else if (request.blobData instanceof Blob) { + // Convert Blob to ArrayBuffer first, then to Base64 + const arrayBuffer = await request.blobData.arrayBuffer(); + data = Utils.fromBufferToB64(arrayBuffer); + } else { + // Already an ArrayBuffer + data = Utils.fromBufferToB64(request.blobData as ArrayBuffer); + } + + await SafariApp.sendMessageToApp( + "downloadFile", + JSON.stringify({ + blobData: data, + blobOptions: request.blobOptions, + fileName: request.fileName, + }), + true, + ); + } } diff --git a/apps/browser/src/safari/safari/SafariWebExtensionHandler.swift b/apps/browser/src/safari/safari/SafariWebExtensionHandler.swift index 54e91611325..dad1e6855fc 100644 --- a/apps/browser/src/safari/safari/SafariWebExtensionHandler.swift +++ b/apps/browser/src/safari/safari/SafariWebExtensionHandler.swift @@ -69,8 +69,8 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { if let url = panel.url { do { let fileManager = FileManager.default - if !fileManager.fileExists(atPath: url.absoluteString) { - fileManager.createFile(atPath: url.absoluteString, contents: Data(), + if !fileManager.fileExists(atPath: url.path) { + fileManager.createFile(atPath: url.path, contents: Data(), attributes: nil) } try data.write(to: url) From c1a988c2abc56e7b031911496272b24478b3d9fe Mon Sep 17 00:00:00 2001 From: Brandon Treston <btreston@bitwarden.com> Date: Tue, 28 Oct 2025 12:25:56 -0400 Subject: [PATCH 67/73] fix DI (#17076) --- .../bulk/bulk-confirm-dialog.component.ts | 4 ++-- .../member-actions/member-actions.service.ts | 4 ++-- .../services/default-auto-confirm.service.ts | 7 ++----- libs/angular/src/services/jslib-services.module.ts | 13 +++++++++++++ 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.ts index 81930279184..3a624e11d95 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.ts @@ -5,11 +5,11 @@ import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { firstValueFrom, map, Observable, switchMap } from "rxjs"; import { - DefaultOrganizationUserService, OrganizationUserApiService, OrganizationUserBulkConfirmRequest, OrganizationUserBulkPublicKeyResponse, OrganizationUserBulkResponse, + OrganizationUserService, } from "@bitwarden/admin-console/common"; import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -53,7 +53,7 @@ export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent { private organizationUserApiService: OrganizationUserApiService, protected i18nService: I18nService, private stateProvider: StateProvider, - private organizationUserService: DefaultOrganizationUserService, + private organizationUserService: OrganizationUserService, private configService: ConfigService, ) { super(keyService, encryptService, i18nService); diff --git a/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.ts b/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.ts index 2913e90e6c0..5e19e26954e 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/member-actions/member-actions.service.ts @@ -2,10 +2,10 @@ import { Injectable } from "@angular/core"; import { firstValueFrom, switchMap, map } from "rxjs"; import { - DefaultOrganizationUserService, OrganizationUserApiService, OrganizationUserBulkResponse, OrganizationUserConfirmRequest, + OrganizationUserService, } from "@bitwarden/admin-console/common"; import { OrganizationUserType, @@ -39,7 +39,7 @@ export class MemberActionsService { constructor( private organizationUserApiService: OrganizationUserApiService, - private organizationUserService: DefaultOrganizationUserService, + private organizationUserService: OrganizationUserService, private keyService: KeyService, private encryptService: EncryptService, private configService: ConfigService, diff --git a/libs/admin-console/src/common/auto-confirm/services/default-auto-confirm.service.ts b/libs/admin-console/src/common/auto-confirm/services/default-auto-confirm.service.ts index a906a2ddc4a..d6c435b84a3 100644 --- a/libs/admin-console/src/common/auto-confirm/services/default-auto-confirm.service.ts +++ b/libs/admin-console/src/common/auto-confirm/services/default-auto-confirm.service.ts @@ -11,10 +11,7 @@ import { OrganizationId } from "@bitwarden/common/types/guid"; import { StateProvider } from "@bitwarden/state"; import { UserId } from "@bitwarden/user-core"; -import { - DefaultOrganizationUserService, - OrganizationUserApiService, -} from "../../organization-user"; +import { OrganizationUserApiService, OrganizationUserService } from "../../organization-user"; import { AutomaticUserConfirmationService } from "../abstractions/auto-confirm.service.abstraction"; import { AUTO_CONFIRM_STATE, AutoConfirmState } from "../models/auto-confirm-state.model"; @@ -22,7 +19,7 @@ export class DefaultAutomaticUserConfirmationService implements AutomaticUserCon constructor( private configService: ConfigService, private apiService: ApiService, - private organizationUserService: DefaultOrganizationUserService, + private organizationUserService: OrganizationUserService, private stateProvider: StateProvider, private organizationService: InternalOrganizationServiceAbstraction, private organizationUserApiService: OrganizationUserApiService, diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 47e9e7d23bd..94b9f6240a4 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -9,7 +9,9 @@ import { CollectionService, DefaultCollectionService, DefaultOrganizationUserApiService, + DefaultOrganizationUserService, OrganizationUserApiService, + OrganizationUserService, } from "@bitwarden/admin-console/common"; import { ChangePasswordService, @@ -1121,6 +1123,17 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultOrganizationService, deps: [StateProvider], }), + safeProvider({ + provide: OrganizationUserService, + useClass: DefaultOrganizationUserService, + deps: [ + KeyService, + EncryptService, + OrganizationUserApiService, + AccountService, + I18nServiceAbstraction, + ], + }), safeProvider({ provide: OrganizationServiceAbstraction, useExisting: InternalOrganizationServiceAbstraction, From 11d3f5247ce287f4d766ab8d733ec0d82010e401 Mon Sep 17 00:00:00 2001 From: Mick Letofsky <mletofsky@bitwarden.com> Date: Tue, 28 Oct 2025 19:00:56 +0100 Subject: [PATCH 68/73] Refactor canClone method to use CipherAuthorizationService (#16849) --- .../vault-items/vault-items.component.html | 2 +- .../vault-items/vault-items.component.ts | 49 ++--- .../restricted-item-types.service.spec.ts | 168 ++++++++++++++++++ 3 files changed, 185 insertions(+), 34 deletions(-) diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.html b/apps/web/src/app/vault/components/vault-items/vault-items.component.html index 23400a7d782..d6b5fafe6ec 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.html @@ -162,7 +162,7 @@ [showPremiumFeatures]="showPremiumFeatures" [useEvents]="useEvents" [viewingOrgVault]="viewingOrgVault" - [cloneable]="canClone(item)" + [cloneable]="canClone$(item) | async" [organizations]="allOrganizations" [collections]="allCollections" [checked]="selection.isSelected(item)" diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts index 9ea4c209009..3ab643927f1 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { SelectionModel } from "@angular/cdk/collections"; import { Component, EventEmitter, Input, Output } from "@angular/core"; -import { toSignal, takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { Observable, combineLatest, map, of, startWith, switchMap } from "rxjs"; import { CollectionView, Unassigned, CollectionAdminView } from "@bitwarden/admin-console/common"; @@ -111,8 +111,6 @@ export class VaultItemsComponent<C extends CipherViewLike> { // eslint-disable-next-line @angular-eslint/prefer-signals @Input() enforceOrgDataOwnershipPolicy: boolean; - private readonly restrictedPolicies = toSignal(this.restrictedItemTypesService.restricted$); - private _ciphers?: C[] = []; // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals // eslint-disable-next-line @angular-eslint/prefer-signals @@ -390,37 +388,22 @@ export class VaultItemsComponent<C extends CipherViewLike> { }); } - // TODO: PM-13944 Refactor to use cipherAuthorizationService.canClone$ instead - protected canClone(vaultItem: VaultItem<C>) { - // This will check for restrictions from org policies before allowing cloning. - const isItemRestricted = this.restrictedPolicies().some( - (rt) => rt.cipherType === CipherViewLikeUtils.getType(vaultItem.cipher), + protected canClone$(vaultItem: VaultItem<C>): Observable<boolean> { + return this.restrictedItemTypesService.restricted$.pipe( + switchMap((restrictedTypes) => { + // This will check for restrictions from org policies before allowing cloning. + const isItemRestricted = restrictedTypes.some( + (rt) => rt.cipherType === CipherViewLikeUtils.getType(vaultItem.cipher), + ); + if (isItemRestricted) { + return of(false); + } + return this.cipherAuthorizationService.canCloneCipher$( + vaultItem.cipher, + this.showAdminActions, + ); + }), ); - if (isItemRestricted) { - return false; - } - - if (vaultItem.cipher.organizationId == null) { - return true; - } - - const org = this.allOrganizations.find((o) => o.id === vaultItem.cipher.organizationId); - - // Admins and custom users can always clone in the Org Vault - if (this.viewingOrgVault && (org.isAdmin || org.permissions.editAnyCollection)) { - return true; - } - - // Check if the cipher belongs to a collection with canManage permission - const orgCollections = this.allCollections.filter((c) => c.organizationId === org.id); - - for (const collection of orgCollections) { - if (vaultItem.cipher.collectionIds.includes(collection.id as any) && collection.manage) { - return true; - } - } - - return false; } protected canEditCipher(cipher: C) { diff --git a/libs/common/src/vault/services/restricted-item-types.service.spec.ts b/libs/common/src/vault/services/restricted-item-types.service.spec.ts index 3ae68d47c5c..c16a91d0884 100644 --- a/libs/common/src/vault/services/restricted-item-types.service.spec.ts +++ b/libs/common/src/vault/services/restricted-item-types.service.spec.ts @@ -12,6 +12,8 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { UserId } from "@bitwarden/common/types/guid"; import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherLike } from "../types/cipher-like"; + import { RestrictedItemTypesService, RestrictedCipherType } from "./restricted-item-types.service"; describe("RestrictedItemTypesService", () => { @@ -130,4 +132,170 @@ describe("RestrictedItemTypesService", () => { { cipherType: CipherType.Identity, allowViewOrgIds: ["org1"] }, ]); }); + + describe("isCipherRestricted", () => { + it("returns false when cipher type is not in restricted types", () => { + const cipher: CipherLike = { + type: CipherType.Login, + organizationId: "Pete the Cat", + } as CipherLike; + const restrictedTypes: RestrictedCipherType[] = [ + { cipherType: CipherType.Card, allowViewOrgIds: [] }, + ]; + + const result = service.isCipherRestricted(cipher, restrictedTypes); + + expect(result).toBe(false); + }); + + it("returns false when restricted types array is empty", () => { + const cipher: CipherLike = { type: CipherType.Card, organizationId: "org1" } as CipherLike; + const restrictedTypes: RestrictedCipherType[] = []; + + const result = service.isCipherRestricted(cipher, restrictedTypes); + + expect(result).toBe(false); + }); + + it("returns false when cipher type does not match any restricted types", () => { + const cipher: CipherLike = { + type: CipherType.SecureNote, + organizationId: "org1", + } as CipherLike; + const restrictedTypes: RestrictedCipherType[] = [ + { cipherType: CipherType.Card, allowViewOrgIds: [] }, + { cipherType: CipherType.Login, allowViewOrgIds: [] }, + { cipherType: CipherType.Identity, allowViewOrgIds: [] }, + ]; + + const result = service.isCipherRestricted(cipher, restrictedTypes); + + expect(result).toBe(false); + }); + + it("returns true for personal cipher when type is restricted", () => { + const cipher: CipherLike = { type: CipherType.Card, organizationId: null } as CipherLike; + const restrictedTypes: RestrictedCipherType[] = [ + { cipherType: CipherType.Card, allowViewOrgIds: ["org1"] }, + ]; + + const result = service.isCipherRestricted(cipher, restrictedTypes); + + expect(result).toBe(true); + }); + + it("returns true for personal cipher with undefined organizationId when type is restricted", () => { + const cipher: CipherLike = { + type: CipherType.Login, + organizationId: undefined, + } as CipherLike; + const restrictedTypes: RestrictedCipherType[] = [ + { cipherType: CipherType.Login, allowViewOrgIds: ["org1", "org2"] }, + ]; + + const result = service.isCipherRestricted(cipher, restrictedTypes); + + expect(result).toBe(true); + }); + + it("returns true for personal cipher regardless of allowViewOrgIds content", () => { + const cipher: CipherLike = { type: CipherType.Identity, organizationId: null } as CipherLike; + const restrictedTypes: RestrictedCipherType[] = [ + { cipherType: CipherType.Identity, allowViewOrgIds: [] }, + ]; + + const result = service.isCipherRestricted(cipher, restrictedTypes); + + expect(result).toBe(true); + }); + + it("returns false when organization is in allowViewOrgIds", () => { + const cipher: CipherLike = { type: CipherType.Card, organizationId: "org1" } as CipherLike; + const restrictedTypes: RestrictedCipherType[] = [ + { cipherType: CipherType.Card, allowViewOrgIds: ["org1"] }, + ]; + + const result = service.isCipherRestricted(cipher, restrictedTypes); + + expect(result).toBe(false); + }); + + it("returns false when organization is among multiple allowViewOrgIds", () => { + const cipher: CipherLike = { type: CipherType.Login, organizationId: "org2" } as CipherLike; + const restrictedTypes: RestrictedCipherType[] = [ + { cipherType: CipherType.Login, allowViewOrgIds: ["org1", "org2", "org3"] }, + ]; + + const result = service.isCipherRestricted(cipher, restrictedTypes); + + expect(result).toBe(false); + }); + + it("returns false when type is restricted globally but cipher org allows it", () => { + const cipher: CipherLike = { type: CipherType.Card, organizationId: "org2" } as CipherLike; + const restrictedTypes: RestrictedCipherType[] = [ + { cipherType: CipherType.Card, allowViewOrgIds: ["org2"] }, + ]; + + const result = service.isCipherRestricted(cipher, restrictedTypes); + + expect(result).toBe(false); + }); + + it("returns true when organization is not in allowViewOrgIds", () => { + const cipher: CipherLike = { type: CipherType.Card, organizationId: "org3" } as CipherLike; + const restrictedTypes: RestrictedCipherType[] = [ + { cipherType: CipherType.Card, allowViewOrgIds: ["org1", "org2"] }, + ]; + + const result = service.isCipherRestricted(cipher, restrictedTypes); + + expect(result).toBe(true); + }); + + it("returns true when allowViewOrgIds is empty for org cipher", () => { + const cipher: CipherLike = { type: CipherType.Login, organizationId: "org1" } as CipherLike; + const restrictedTypes: RestrictedCipherType[] = [ + { cipherType: CipherType.Login, allowViewOrgIds: [] }, + ]; + + const result = service.isCipherRestricted(cipher, restrictedTypes); + + expect(result).toBe(true); + }); + + it("returns true when cipher org differs from all allowViewOrgIds", () => { + const cipher: CipherLike = { + type: CipherType.Identity, + organizationId: "org5", + } as CipherLike; + const restrictedTypes: RestrictedCipherType[] = [ + { cipherType: CipherType.Identity, allowViewOrgIds: ["org1", "org2", "org3", "org4"] }, + ]; + + const result = service.isCipherRestricted(cipher, restrictedTypes); + + expect(result).toBe(true); + }); + }); + + describe("isCipherRestricted$", () => { + it("returns true when cipher is restricted by policy", async () => { + policyService.policiesByType$.mockReturnValue(of([policyOrg1])); + const cipher: CipherLike = { type: CipherType.Card, organizationId: null } as CipherLike; + + const result = await firstValueFrom(service.isCipherRestricted$(cipher)); + + expect(result).toBe(true); + }); + + it("returns false when cipher is not restricted", async () => { + policyService.policiesByType$.mockReturnValue(of([policyOrg1])); + const cipher: CipherLike = { type: CipherType.Login, organizationId: "org2" } as CipherLike; + + const result = await firstValueFrom(service.isCipherRestricted$(cipher)); + + expect(result).toBe(false); + }); + }); }); From 185c912c620d66892b69e4d1ccfbe04de3fcbc35 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 14:46:17 -0400 Subject: [PATCH 69/73] [deps]: Update peter-evans/repository-dispatch action to v4 (#16848) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test-browser-interactions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-browser-interactions.yml b/.github/workflows/test-browser-interactions.yml index a5b92563f5a..fb31a93d51f 100644 --- a/.github/workflows/test-browser-interactions.yml +++ b/.github/workflows/test-browser-interactions.yml @@ -73,7 +73,7 @@ jobs: - name: Trigger test-all workflow in browser-interactions-testing if: steps.changed-files.outputs.monitored == 'true' - uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0 + uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4.0.0 with: token: ${{ steps.app-token.outputs.token }} repository: "bitwarden/browser-interactions-testing" From fe26826369f65cca5480fc15fe38688a252e4cf1 Mon Sep 17 00:00:00 2001 From: Daniel Riera <driera@livefront.com> Date: Tue, 28 Oct 2025 14:47:49 -0400 Subject: [PATCH 70/73] PM-27366 drop scss and convert to vanilla css (#17046) --- .../bootstrap-autofill-inline-menu-button.ts | 5 +---- .../pages/button/{button.scss => button.css} | 14 ++++++-------- 2 files changed, 7 insertions(+), 12 deletions(-) rename apps/browser/src/autofill/overlay/inline-menu/pages/button/{button.scss => button.css} (74%) diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/button/bootstrap-autofill-inline-menu-button.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/button/bootstrap-autofill-inline-menu-button.ts index 36ef3897c56..dffacce0ffc 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/button/bootstrap-autofill-inline-menu-button.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/button/bootstrap-autofill-inline-menu-button.ts @@ -1,10 +1,7 @@ import { AutofillOverlayElement } from "../../../../enums/autofill-overlay.enum"; import { AutofillInlineMenuButton } from "./autofill-inline-menu-button"; - -// FIXME: Remove when updating file. Eslint update -// eslint-disable-next-line @typescript-eslint/no-require-imports -require("./button.scss"); +import "./button.css"; (function () { globalThis.customElements.define(AutofillOverlayElement.Button, AutofillInlineMenuButton); diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/button/button.scss b/apps/browser/src/autofill/overlay/inline-menu/pages/button/button.css similarity index 74% rename from apps/browser/src/autofill/overlay/inline-menu/pages/button/button.scss rename to apps/browser/src/autofill/overlay/inline-menu/pages/button/button.css index 64e54179893..a1fce6f14da 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/button/button.scss +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/button/button.css @@ -1,5 +1,3 @@ -@import "../../../../shared/styles/variables"; - * { box-sizing: border-box; } @@ -27,10 +25,10 @@ autofill-inline-menu-button { border: none; background: transparent; cursor: pointer; - - .inline-menu-button-svg-icon { - display: block; - width: 100%; - height: auto; - } +} + +.inline-menu-button .inline-menu-button-svg-icon { + display: block; + width: 100%; + height: auto; } From af1809222f1cc7719b9119363bcd95c21a7ce324 Mon Sep 17 00:00:00 2001 From: Dave <3836813+enmande@users.noreply.github.com> Date: Tue, 28 Oct 2025 15:14:48 -0400 Subject: [PATCH 71/73] fix(input-password-component) [PM-24266]: Update modal title. (#16932) --- .../auth/src/angular/input-password/input-password.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/auth/src/angular/input-password/input-password.component.ts b/libs/auth/src/angular/input-password/input-password.component.ts index 019a9e3975e..62294f037a0 100644 --- a/libs/auth/src/angular/input-password/input-password.component.ts +++ b/libs/auth/src/angular/input-password/input-password.component.ts @@ -564,7 +564,7 @@ export class InputPasswordComponent implements OnInit { } } else if (passwordIsWeak) { const userAcceptedDialog = await this.dialogService.openSimpleDialog({ - title: { key: "weakMasterPasswordDesc" }, + title: { key: "weakMasterPassword" }, content: { key: "weakMasterPasswordDesc" }, type: "warning", }); From 69d5c533ef40b510c93a32fc61bef3166c6030ae Mon Sep 17 00:00:00 2001 From: Mick Letofsky <mletofsky@bitwarden.com> Date: Tue, 28 Oct 2025 20:19:41 +0100 Subject: [PATCH 72/73] Implement Claude respond reusable workflow (#17079) --- .github/workflows/respond.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/respond.yml diff --git a/.github/workflows/respond.yml b/.github/workflows/respond.yml new file mode 100644 index 00000000000..d940ceee756 --- /dev/null +++ b/.github/workflows/respond.yml @@ -0,0 +1,28 @@ +name: Respond + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +permissions: {} + +jobs: + respond: + name: Respond + uses: bitwarden/gh-actions/.github/workflows/_respond.yml@main + secrets: + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + permissions: + actions: read + contents: write + id-token: write + issues: write + pull-requests: write From ff30df3dd6244e2ed5a1605c179e7c4d33bb1edc Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Tue, 28 Oct 2025 20:28:34 +0100 Subject: [PATCH 73/73] [PM-19300] Session timeout policy (#16583) * Session timeout policy * default "custom" is 8 hours, validation fixes * ownership update * default max allowed timeout is not selected * adjusting defaults, fixing backwards compatibility, skip type confirmation dialog when switching between the never and on system lock * unit test coverage * wording update, custom hours, minutes jumping on errors * wording update * wrong session timeout action dropdown label * show dialog as valid when opened first time, use @for loop, use controls instead of get * dialog static opener * easier to understand type value listener * unit tests * explicit maximum allowed timeout required error * eslint revert --- .github/CODEOWNERS | 1 + apps/web/src/locales/en/messages.json | 46 +- .../policies/policy-edit-definitions/index.ts | 1 - .../maximum-vault-timeout.component.html | 32 -- .../maximum-vault-timeout.component.ts | 79 ---- .../policies/policy-edit-register.ts | 4 +- ...-timeout-confirmation-never.component.html | 38 ++ ...meout-confirmation-never.component.spec.ts | 79 ++++ ...on-timeout-confirmation-never.component.ts | 18 + .../policies/session-timeout.component.html | 39 ++ .../session-timeout.component.spec.ts | 441 ++++++++++++++++++ .../policies/session-timeout.component.ts | 197 ++++++++ 12 files changed, 853 insertions(+), 122 deletions(-) delete mode 100644 bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/maximum-vault-timeout.component.html delete mode 100644 bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/maximum-vault-timeout.component.ts create mode 100644 bitwarden_license/bit-web/src/app/key-management/policies/session-timeout-confirmation-never.component.html create mode 100644 bitwarden_license/bit-web/src/app/key-management/policies/session-timeout-confirmation-never.component.spec.ts create mode 100644 bitwarden_license/bit-web/src/app/key-management/policies/session-timeout-confirmation-never.component.ts create mode 100644 bitwarden_license/bit-web/src/app/key-management/policies/session-timeout.component.html create mode 100644 bitwarden_license/bit-web/src/app/key-management/policies/session-timeout.component.spec.ts create mode 100644 bitwarden_license/bit-web/src/app/key-management/policies/session-timeout.component.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8affac3387b..676c4b4657b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -174,6 +174,7 @@ apps/desktop/src/key-management @bitwarden/team-key-management-dev apps/web/src/app/key-management @bitwarden/team-key-management-dev apps/browser/src/key-management @bitwarden/team-key-management-dev apps/cli/src/key-management @bitwarden/team-key-management-dev +bitwarden_license/bit-web/src/app/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 diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 72ca4d73976..aa0353e754d 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -6495,17 +6495,32 @@ "tdeDisabledMasterPasswordRequired": { "message": "Your organization has updated your decryption options. Please set a master password to access your vault." }, - "maximumVaultTimeout": { - "message": "Vault timeout" + "sessionTimeoutPolicyTitle": { + "message": "Session timeout" }, - "maximumVaultTimeoutDesc": { - "message": "Set a maximum vault timeout for members." + "sessionTimeoutPolicyDescription": { + "message": "Set a maximum session timeout for all members except owners." }, - "maximumVaultTimeoutLabel": { - "message": "Maximum vault timeout" + "maximumAllowedTimeout": { + "message": "Maximum allowed timeout" }, - "invalidMaximumVaultTimeout": { - "message": "Invalid maximum vault timeout." + "maximumAllowedTimeoutRequired": { + "message": "Maximum allowed timeout is required." + }, + "sessionTimeoutPolicyInvalidTime": { + "message": "Time is invalid. Change at least one value." + }, + "sessionTimeoutAction": { + "message": "Session timeout action" + }, + "immediately": { + "message": "Immediately" + }, + "onSystemLock": { + "message": "On system lock" + }, + "onAppRestart": { + "message": "On app restart" }, "hours": { "message": "Hours" @@ -6513,6 +6528,21 @@ "minutes": { "message": "Minutes" }, + "sessionTimeoutConfirmationNeverTitle": { + "message": "Are you certain you want to allow a maximum timeout of \"Never\" for all members?" + }, + "sessionTimeoutConfirmationNeverDescription": { + "message": "This option will save your members' encryption keys on their devices. If you choose this option, ensure that their devices are adequately protected." + }, + "learnMoreAboutDeviceProtection": { + "message": "Learn more about device protection" + }, + "sessionTimeoutConfirmationOnSystemLockTitle": { + "message": "\"System lock\" will only apply to the browser and desktop app" + }, + "sessionTimeoutConfirmationOnSystemLockDescription": { + "message": "The mobile and web app will use \"on app restart\" as their maximum allowed timeout, since the option is not supported." + }, "vaultTimeoutPolicyInEffect": { "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", "placeholders": { diff --git a/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/index.ts b/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/index.ts index 8c4be2eeea1..52325eae160 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/index.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/index.ts @@ -1,4 +1,3 @@ export { ActivateAutofillPolicy } from "./activate-autofill.component"; export { AutomaticAppLoginPolicy } from "./automatic-app-login.component"; export { DisablePersonalVaultExportPolicy } from "./disable-personal-vault-export.component"; -export { MaximumVaultTimeoutPolicy } from "./maximum-vault-timeout.component"; diff --git a/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/maximum-vault-timeout.component.html b/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/maximum-vault-timeout.component.html deleted file mode 100644 index deb72cfb3b5..00000000000 --- a/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/maximum-vault-timeout.component.html +++ /dev/null @@ -1,32 +0,0 @@ -<bit-callout title="{{ 'prerequisite' | i18n }}"> - {{ "requireSsoPolicyReq" | i18n }} -</bit-callout> - -<bit-form-control> - <input type="checkbox" id="enabled" bitCheckbox [formControl]="enabled" /> - <bit-label>{{ "turnOn" | i18n }}</bit-label> -</bit-form-control> - -<div [formGroup]="data"> - <div class="tw-grid tw-grid-cols-12 tw-gap-4"> - <bit-form-field class="tw-col-span-6 !tw-mb-0"> - <bit-label>{{ "maximumVaultTimeoutLabel" | i18n }}</bit-label> - <input bitInput type="number" min="0" formControlName="hours" /> - <bit-hint>{{ "hours" | i18n }}</bit-hint> - </bit-form-field> - <bit-form-field class="tw-col-span-6 tw-self-end !tw-mb-0"> - <input bitInput type="number" min="0" max="59" formControlName="minutes" /> - <bit-hint>{{ "minutes" | i18n }}</bit-hint> - </bit-form-field> - <bit-form-field class="tw-col-span-6"> - <bit-label>{{ "vaultTimeoutAction" | i18n }}</bit-label> - <bit-select formControlName="action"> - <bit-option - *ngFor="let option of vaultTimeoutActionOptions" - [value]="option.value" - [label]="option.name" - ></bit-option> - </bit-select> - </bit-form-field> - </div> -</div> diff --git a/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/maximum-vault-timeout.component.ts b/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/maximum-vault-timeout.component.ts deleted file mode 100644 index 277388e2883..00000000000 --- a/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/maximum-vault-timeout.component.ts +++ /dev/null @@ -1,79 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component } from "@angular/core"; -import { FormBuilder, FormControl } from "@angular/forms"; - -import { PolicyType } from "@bitwarden/common/admin-console/enums"; -import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request"; -import { VaultTimeoutAction } from "@bitwarden/common/key-management/vault-timeout"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { - BasePolicyEditDefinition, - BasePolicyEditComponent, -} from "@bitwarden/web-vault/app/admin-console/organizations/policies"; -import { SharedModule } from "@bitwarden/web-vault/app/shared"; - -export class MaximumVaultTimeoutPolicy extends BasePolicyEditDefinition { - name = "maximumVaultTimeout"; - description = "maximumVaultTimeoutDesc"; - type = PolicyType.MaximumVaultTimeout; - component = MaximumVaultTimeoutPolicyComponent; -} - -// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush -// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -@Component({ - templateUrl: "maximum-vault-timeout.component.html", - imports: [SharedModule], -}) -export class MaximumVaultTimeoutPolicyComponent extends BasePolicyEditComponent { - vaultTimeoutActionOptions: { name: string; value: string }[]; - data = this.formBuilder.group({ - hours: new FormControl<number>(null), - minutes: new FormControl<number>(null), - action: new FormControl<string>(null), - }); - - constructor( - private formBuilder: FormBuilder, - private i18nService: I18nService, - ) { - super(); - this.vaultTimeoutActionOptions = [ - { name: i18nService.t("userPreference"), value: null }, - { name: i18nService.t(VaultTimeoutAction.Lock), value: VaultTimeoutAction.Lock }, - { name: i18nService.t(VaultTimeoutAction.LogOut), value: VaultTimeoutAction.LogOut }, - ]; - } - - protected loadData() { - const minutes = this.policyResponse.data?.minutes; - const action = this.policyResponse.data?.action; - - this.data.patchValue({ - hours: minutes ? Math.floor(minutes / 60) : null, - minutes: minutes ? minutes % 60 : null, - action: action, - }); - } - - protected buildRequestData() { - if (this.data.value.hours == null && this.data.value.minutes == null) { - return null; - } - - return { - minutes: this.data.value.hours * 60 + this.data.value.minutes, - action: this.data.value.action, - }; - } - - async buildRequest(): Promise<PolicyRequest> { - const request = await super.buildRequest(); - if (request.data?.minutes == null || request.data?.minutes <= 0) { - throw new Error(this.i18nService.t("invalidMaximumVaultTimeout")); - } - - return request; - } -} diff --git a/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-register.ts b/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-register.ts index 3438e706f10..015b4fc17be 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-register.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-register.ts @@ -4,12 +4,12 @@ import { } from "@bitwarden/web-vault/app/admin-console/organizations/policies"; import { FreeFamiliesSponsorshipPolicy } from "../../billing/policies/free-families-sponsorship.component"; +import { SessionTimeoutPolicy } from "../../key-management/policies/session-timeout.component"; import { ActivateAutofillPolicy, AutomaticAppLoginPolicy, DisablePersonalVaultExportPolicy, - MaximumVaultTimeoutPolicy, } from "./policy-edit-definitions"; /** @@ -18,7 +18,7 @@ import { * It will not appear in the web vault when running in OSS mode. */ const policyEditRegister: BasePolicyEditDefinition[] = [ - new MaximumVaultTimeoutPolicy(), + new SessionTimeoutPolicy(), new DisablePersonalVaultExportPolicy(), new FreeFamiliesSponsorshipPolicy(), new ActivateAutofillPolicy(), diff --git a/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout-confirmation-never.component.html b/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout-confirmation-never.component.html new file mode 100644 index 00000000000..2b718990c30 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout-confirmation-never.component.html @@ -0,0 +1,38 @@ +<bit-dialog dialogSize="small"> + <div bitDialogTitle class="tw-mt-4 tw-flex tw-flex-col tw-gap-2 tw-text-center"> + <i class="bwi bwi-exclamation-triangle tw-text-3xl tw-text-warning" aria-hidden="true"></i> + <h1 + bitTypography="h3" + class="tw-break-words tw-hyphens-auto tw-whitespace-normal tw-max-w-fit tw-inline-block" + > + {{ "sessionTimeoutConfirmationNeverTitle" | i18n }} + </h1> + </div> + + <span + bitDialogContent + class="tw-flex tw-flex-col tw-gap-2 tw-items-center tw-text-center tw-text-base tw-break-words tw-hyphens-auto" + > + <p>{{ "sessionTimeoutConfirmationNeverDescription" | i18n }}</p> + <a + target="_blank" + rel="noreferrer" + appA11yTitle="{{ 'learnMoreAboutDeviceProtection' | i18n }}" + href="https://bitwarden.com/help/vault-timeout/" + bitLink + class="tw-flex tw-flex-row tw-gap-1" + > + {{ "learnMoreAboutDeviceProtection" | i18n }} + <i class="bwi bwi-external-link" aria-hidden="true"></i> + </a> + </span> + + <div bitDialogFooter class="tw-flex tw-flex-col tw-flex-grow tw-gap-2"> + <button bitButton buttonType="primary" type="button" (click)="dialogRef.close(true)"> + {{ "yes" | i18n }} + </button> + <button bitButton buttonType="secondary" type="button" (click)="dialogRef.close(false)"> + {{ "no" | i18n }} + </button> + </div> +</bit-dialog> diff --git a/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout-confirmation-never.component.spec.ts b/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout-confirmation-never.component.spec.ts new file mode 100644 index 00000000000..332a0e323a7 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout-confirmation-never.component.spec.ts @@ -0,0 +1,79 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; +import { mock } from "jest-mock-extended"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogRef, DialogService } from "@bitwarden/components"; + +import { SessionTimeoutConfirmationNeverComponent } from "./session-timeout-confirmation-never.component"; + +describe("SessionTimeoutConfirmationNeverComponent", () => { + let component: SessionTimeoutConfirmationNeverComponent; + let fixture: ComponentFixture<SessionTimeoutConfirmationNeverComponent>; + let mockDialogRef: jest.Mocked<DialogRef>; + + const mockI18nService = mock<I18nService>(); + const mockDialogService = mock<DialogService>(); + + beforeEach(async () => { + mockDialogRef = mock<DialogRef>(); + mockI18nService.t.mockImplementation((key) => `${key}-used-i18n`); + + await TestBed.configureTestingModule({ + imports: [SessionTimeoutConfirmationNeverComponent, NoopAnimationsModule], + providers: [ + { provide: DialogRef, useValue: mockDialogRef }, + { provide: I18nService, useValue: mockI18nService }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(SessionTimeoutConfirmationNeverComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); + + describe("open", () => { + it("should call dialogService.open with correct parameters", () => { + const mockResult = mock<DialogRef>(); + mockDialogService.open.mockReturnValue(mockResult); + + const result = SessionTimeoutConfirmationNeverComponent.open(mockDialogService); + + expect(mockDialogService.open).toHaveBeenCalledWith( + SessionTimeoutConfirmationNeverComponent, + { + disableClose: true, + }, + ); + expect(result).toBe(mockResult); + }); + }); + + describe("button clicks", () => { + it("should close dialog with true when Yes button is clicked", () => { + const yesButton = fixture.nativeElement.querySelector( + 'button[buttonType="primary"]', + ) as HTMLButtonElement; + + yesButton.click(); + + expect(mockDialogRef.close).toHaveBeenCalledWith(true); + expect(yesButton.textContent?.trim()).toBe("yes-used-i18n"); + }); + + it("should close dialog with false when No button is clicked", () => { + const noButton = fixture.nativeElement.querySelector( + 'button[buttonType="secondary"]', + ) as HTMLButtonElement; + + noButton.click(); + + expect(mockDialogRef.close).toHaveBeenCalledWith(false); + expect(noButton.textContent?.trim()).toBe("no-used-i18n"); + }); + }); +}); diff --git a/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout-confirmation-never.component.ts b/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout-confirmation-never.component.ts new file mode 100644 index 00000000000..a909baf1c77 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout-confirmation-never.component.ts @@ -0,0 +1,18 @@ +import { Component } from "@angular/core"; + +import { DialogRef, DialogService } from "@bitwarden/components"; +import { SharedModule } from "@bitwarden/web-vault/app/shared"; + +@Component({ + imports: [SharedModule], + templateUrl: "./session-timeout-confirmation-never.component.html", +}) +export class SessionTimeoutConfirmationNeverComponent { + constructor(public dialogRef: DialogRef) {} + + static open(dialogService: DialogService) { + return dialogService.open<boolean>(SessionTimeoutConfirmationNeverComponent, { + disableClose: true, + }); + } +} diff --git a/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout.component.html b/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout.component.html new file mode 100644 index 00000000000..22e9e07bea7 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout.component.html @@ -0,0 +1,39 @@ +<bit-callout title="{{ 'prerequisite' | i18n }}"> + {{ "requireSsoPolicyReq" | i18n }} +</bit-callout> + +<bit-form-control> + <input type="checkbox" id="enabled" bitCheckbox [formControl]="enabled" /> + <bit-label>{{ "turnOn" | i18n }}</bit-label> +</bit-form-control> + +<div [formGroup]="data"> + <div class="tw-grid tw-grid-cols-12 tw-gap-4"> + <bit-form-field class="tw-col-span-12 !tw-mb-0"> + <bit-label>{{ "maximumAllowedTimeout" | i18n }}</bit-label> + <bit-select formControlName="type"> + @for (option of typeOptions; track option.value) { + <bit-option [value]="option.value" [label]="option.name"></bit-option> + } + </bit-select> + </bit-form-field> + @if (data.value.type === "custom") { + <bit-form-field class="tw-col-span-6 tw-self-start !tw-mb-0"> + <bit-label>{{ "hours" | i18n }}</bit-label> + <input bitInput type="number" min="0" formControlName="hours" /> + </bit-form-field> + <bit-form-field class="tw-col-span-6 tw-self-start !tw-mb-0"> + <bit-label>{{ "minutes" | i18n }}</bit-label> + <input bitInput type="number" min="0" max="59" formControlName="minutes" /> + </bit-form-field> + } + <bit-form-field class="tw-col-span-12"> + <bit-label>{{ "sessionTimeoutAction" | i18n }}</bit-label> + <bit-select formControlName="action"> + @for (option of actionOptions; track option.value) { + <bit-option [value]="option.value" [label]="option.name"></bit-option> + } + </bit-select> + </bit-form-field> + </div> +</div> diff --git a/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout.component.spec.ts b/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout.component.spec.ts new file mode 100644 index 00000000000..694b0f1d1a2 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout.component.spec.ts @@ -0,0 +1,441 @@ +import { DialogCloseOptions } from "@angular/cdk/dialog"; +import { DebugElement } from "@angular/core"; +import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; +import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; +import { By } from "@angular/platform-browser"; +import { mock } from "jest-mock-extended"; +import { Observable, of } from "rxjs"; + +import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response"; +import { VaultTimeoutAction } from "@bitwarden/common/key-management/vault-timeout"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogRef, DialogService } from "@bitwarden/components"; + +import { SessionTimeoutConfirmationNeverComponent } from "./session-timeout-confirmation-never.component"; +import { + SessionTimeoutAction, + SessionTimeoutPolicyComponent, + SessionTimeoutType, +} from "./session-timeout.component"; + +// Mock DialogRef, so we can mock "readonly closed" property. +class MockDialogRef extends DialogRef { + close(result: unknown | undefined, options: DialogCloseOptions | undefined): void {} + + closed: Observable<unknown | undefined> = of(); + componentInstance: unknown | null; + disableClose: boolean | undefined; + isDrawer: boolean = false; +} + +describe("SessionTimeoutPolicyComponent", () => { + let component: SessionTimeoutPolicyComponent; + let fixture: ComponentFixture<SessionTimeoutPolicyComponent>; + + const mockI18nService = mock<I18nService>(); + const mockDialogService = mock<DialogService>(); + const mockDialogRef = mock<MockDialogRef>(); + + beforeEach(async () => { + jest.resetAllMocks(); + + mockDialogRef.closed = of(true); + mockDialogService.open.mockReturnValue(mockDialogRef); + mockDialogService.openSimpleDialog.mockResolvedValue(true); + + mockI18nService.t.mockImplementation((key) => `${key}-used-i18n`); + + const testBed = TestBed.configureTestingModule({ + imports: [SessionTimeoutPolicyComponent, ReactiveFormsModule], + providers: [FormBuilder, { provide: I18nService, useValue: mockI18nService }], + }); + + // Override DialogService provided from SharedModule (which includes DialogModule) + testBed.overrideProvider(DialogService, { useValue: mockDialogService }); + + await testBed.compileComponents(); + + fixture = TestBed.createComponent(SessionTimeoutPolicyComponent); + component = fixture.componentInstance; + }); + + function assertHoursAndMinutesInputsNotVisible() { + const hoursInput = fixture.nativeElement.querySelector('input[formControlName="hours"]'); + const minutesInput = fixture.nativeElement.querySelector('input[formControlName="minutes"]'); + + expect(hoursInput).toBeFalsy(); + expect(minutesInput).toBeFalsy(); + } + + function assertHoursAndMinutesInputs(expectedHours: string, expectedMinutes: string) { + const hoursInput = fixture.nativeElement.querySelector('input[formControlName="hours"]'); + const minutesInput = fixture.nativeElement.querySelector('input[formControlName="minutes"]'); + + expect(hoursInput).toBeTruthy(); + expect(minutesInput).toBeTruthy(); + expect(hoursInput.disabled).toBe(false); + expect(minutesInput.disabled).toBe(false); + expect(hoursInput.value).toBe(expectedHours); + expect(minutesInput.value).toBe(expectedMinutes); + } + + function setPolicyResponseType(type: SessionTimeoutType) { + component.policyResponse = new PolicyResponse({ + Data: { + type, + minutes: 480, + action: null, + }, + }); + } + + describe("initialization and data loading", () => { + function assertTypeAndActionSelectElementsVisible() { + // Type and action selects should always be present + const typeSelectDebug: DebugElement = fixture.debugElement.query( + By.css('bit-select[formControlName="type"]'), + ); + const actionSelectDebug: DebugElement = fixture.debugElement.query( + By.css('bit-select[formControlName="action"]'), + ); + + expect(typeSelectDebug).toBeTruthy(); + expect(actionSelectDebug).toBeTruthy(); + } + + it("should initialize with default state when policy have no value", () => { + component.policyResponse = undefined; + + fixture.detectChanges(); + + expect(component.data.controls.type.value).toBeNull(); + expect(component.data.controls.type.hasError("required")).toBe(true); + expect(component.data.controls.hours.value).toBe(8); + expect(component.data.controls.hours.disabled).toBe(true); + expect(component.data.controls.minutes.value).toBe(0); + expect(component.data.controls.minutes.disabled).toBe(true); + expect(component.data.controls.action.value).toBeNull(); + + assertTypeAndActionSelectElementsVisible(); + assertHoursAndMinutesInputsNotVisible(); + }); + + // This is for backward compatibility when type field did not exist + it("should load as custom type when type field does not exist but minutes does", () => { + component.policyResponse = new PolicyResponse({ + Data: { + minutes: 500, + action: VaultTimeoutAction.Lock, + }, + }); + + fixture.detectChanges(); + + expect(component.data.controls.type.value).toBe("custom"); + expect(component.data.controls.hours.value).toBe(8); + expect(component.data.controls.hours.disabled).toBe(false); + expect(component.data.controls.minutes.value).toBe(20); + expect(component.data.controls.minutes.disabled).toBe(false); + expect(component.data.controls.action.value).toBe(VaultTimeoutAction.Lock); + + assertTypeAndActionSelectElementsVisible(); + assertHoursAndMinutesInputs("8", "20"); + }); + + it.each([ + ["never", null], + ["never", VaultTimeoutAction.Lock], + ["never", VaultTimeoutAction.LogOut], + ["onAppRestart", null], + ["onAppRestart", VaultTimeoutAction.Lock], + ["onAppRestart", VaultTimeoutAction.LogOut], + ["onSystemLock", null], + ["onSystemLock", VaultTimeoutAction.Lock], + ["onSystemLock", VaultTimeoutAction.LogOut], + ["immediately", null], + ["immediately", VaultTimeoutAction.Lock], + ["immediately", VaultTimeoutAction.LogOut], + ["custom", null], + ["custom", VaultTimeoutAction.Lock], + ["custom", VaultTimeoutAction.LogOut], + ])("should load correctly when policy type is %s and action is %s", (type, action) => { + component.policyResponse = new PolicyResponse({ + Data: { + type, + minutes: 510, + action, + }, + }); + + fixture.detectChanges(); + + expect(component.data.controls.type.value).toBe(type); + expect(component.data.controls.action.value).toBe(action); + + assertTypeAndActionSelectElementsVisible(); + + if (type === "custom") { + expect(component.data.controls.hours.value).toBe(8); + expect(component.data.controls.minutes.value).toBe(30); + expect(component.data.controls.hours.disabled).toBe(false); + expect(component.data.controls.minutes.disabled).toBe(false); + + assertHoursAndMinutesInputs("8", "30"); + } else { + expect(component.data.controls.hours.disabled).toBe(true); + expect(component.data.controls.minutes.disabled).toBe(true); + + assertHoursAndMinutesInputsNotVisible(); + } + }); + + it("should have all type options and update form control when value changes", fakeAsync(() => { + expect(component.typeOptions.length).toBe(5); + expect(component.typeOptions[0].value).toBe("immediately"); + expect(component.typeOptions[1].value).toBe("custom"); + expect(component.typeOptions[2].value).toBe("onSystemLock"); + expect(component.typeOptions[3].value).toBe("onAppRestart"); + expect(component.typeOptions[4].value).toBe("never"); + })); + + it("should have all action options and update form control when value changes", () => { + expect(component.actionOptions.length).toBe(3); + expect(component.actionOptions[0].value).toBeNull(); + expect(component.actionOptions[1].value).toBe(VaultTimeoutAction.Lock); + expect(component.actionOptions[2].value).toBe(VaultTimeoutAction.LogOut); + }); + }); + + describe("form controls change detection", () => { + it.each(["never", "onAppRestart", "onSystemLock", "immediately"])( + "should disable hours and minutes inputs when type changes from custom to %s", + fakeAsync((newType: SessionTimeoutType) => { + setPolicyResponseType("custom"); + fixture.detectChanges(); + + expect(component.data.controls.hours.value).toBe(8); + expect(component.data.controls.minutes.value).toBe(0); + expect(component.data.controls.hours.disabled).toBe(false); + expect(component.data.controls.minutes.disabled).toBe(false); + + component.data.patchValue({ type: newType }); + tick(); + fixture.detectChanges(); + + expect(component.data.controls.hours.disabled).toBe(true); + expect(component.data.controls.minutes.disabled).toBe(true); + + assertHoursAndMinutesInputsNotVisible(); + }), + ); + + it.each(["never", "onAppRestart", "onSystemLock", "immediately"])( + "should enable hours and minutes inputs when type changes from %s to custom", + fakeAsync((oldType: SessionTimeoutType) => { + setPolicyResponseType(oldType); + fixture.detectChanges(); + + expect(component.data.controls.hours.disabled).toBe(true); + expect(component.data.controls.minutes.disabled).toBe(true); + + component.data.patchValue({ type: "custom", hours: 8, minutes: 1 }); + tick(); + fixture.detectChanges(); + + expect(component.data.controls.hours.value).toBe(8); + expect(component.data.controls.minutes.value).toBe(1); + expect(component.data.controls.hours.disabled).toBe(false); + expect(component.data.controls.minutes.disabled).toBe(false); + + assertHoursAndMinutesInputs("8", "1"); + }), + ); + + it.each(["custom", "onAppRestart", "immediately"])( + "should not show confirmation dialog when changing to %s type", + fakeAsync((newType: SessionTimeoutType) => { + setPolicyResponseType(null); + fixture.detectChanges(); + + component.data.patchValue({ type: newType }); + tick(); + fixture.detectChanges(); + + expect(mockDialogService.open).not.toHaveBeenCalled(); + expect(mockDialogService.openSimpleDialog).not.toHaveBeenCalled(); + }), + ); + + it("should show never confirmation dialog when changing to never type", fakeAsync(() => { + setPolicyResponseType(null); + fixture.detectChanges(); + + component.data.patchValue({ type: "never" }); + tick(); + fixture.detectChanges(); + + expect(mockDialogService.open).toHaveBeenCalledWith( + SessionTimeoutConfirmationNeverComponent, + { + disableClose: true, + }, + ); + expect(mockDialogService.openSimpleDialog).not.toHaveBeenCalled(); + })); + + it("should show simple confirmation dialog when changing to onSystemLock type", fakeAsync(() => { + setPolicyResponseType(null); + fixture.detectChanges(); + + component.data.patchValue({ type: "onSystemLock" }); + tick(); + fixture.detectChanges(); + + expect(mockDialogService.openSimpleDialog).toHaveBeenCalledWith({ + type: "info", + title: { key: "sessionTimeoutConfirmationOnSystemLockTitle" }, + content: { key: "sessionTimeoutConfirmationOnSystemLockDescription" }, + acceptButtonText: { key: "continue" }, + cancelButtonText: { key: "cancel" }, + }); + expect(mockDialogService.open).not.toHaveBeenCalled(); + expect(component.data.controls.type.value).toBe("onSystemLock"); + })); + + it("should revert to previous type when type changed to never and dialog not confirmed", fakeAsync(() => { + mockDialogRef.closed = of(false); + setPolicyResponseType("immediately"); + fixture.detectChanges(); + + component.data.patchValue({ type: "never" }); + tick(); + fixture.detectChanges(); + + expect(mockDialogService.open).toHaveBeenCalled(); + expect(mockDialogService.openSimpleDialog).not.toHaveBeenCalled(); + expect(component.data.controls.type.value).toBe("immediately"); + })); + + it("should revert to previous type when type changed to onSystemLock and dialog not confirmed", fakeAsync(() => { + mockDialogService.openSimpleDialog.mockResolvedValue(false); + setPolicyResponseType("immediately"); + fixture.detectChanges(); + + component.data.patchValue({ type: "onSystemLock" }); + tick(); + fixture.detectChanges(); + + expect(mockDialogService.openSimpleDialog).toHaveBeenCalled(); + expect(mockDialogService.open).not.toHaveBeenCalled(); + expect(component.data.controls.type.value).toBe("immediately"); + })); + + it("should revert to last confirmed type when canceling multiple times", fakeAsync(() => { + mockDialogRef.closed = of(false); + mockDialogService.openSimpleDialog.mockResolvedValue(false); + + setPolicyResponseType("custom"); + fixture.detectChanges(); + + // First attempt: custom -> never (cancel) + component.data.patchValue({ type: "never" }); + tick(); + fixture.detectChanges(); + + expect(component.data.controls.type.value).toBe("custom"); + + // Second attempt: custom -> onSystemLock (cancel) + component.data.patchValue({ type: "onSystemLock" }); + tick(); + fixture.detectChanges(); + + // Should revert to "custom", not "never" + expect(component.data.controls.type.value).toBe("custom"); + })); + }); + + describe("buildRequestData", () => { + beforeEach(() => { + setPolicyResponseType("custom"); + fixture.detectChanges(); + }); + + it("should throw max allowed timeout required error when type is invalid", () => { + component.data.patchValue({ type: null }); + + expect(() => component["buildRequestData"]()).toThrow( + "maximumAllowedTimeoutRequired-used-i18n", + ); + }); + + it.each([ + [null, null], + [null, 0], + [0, null], + [0, 0], + ])( + "should throw invalid time error when type is custom, hours is %o and minutes is %o ", + (hours, minutes) => { + component.data.patchValue({ + type: "custom", + hours: hours, + minutes: minutes, + }); + + expect(() => component["buildRequestData"]()).toThrow( + "sessionTimeoutPolicyInvalidTime-used-i18n", + ); + }, + ); + + it("should return correct data when type is custom with valid time", () => { + component.data.patchValue({ + type: "custom", + hours: 8, + minutes: 30, + action: VaultTimeoutAction.Lock, + }); + + const result = component["buildRequestData"](); + + expect(result).toEqual({ + type: "custom", + minutes: 510, + action: VaultTimeoutAction.Lock, + }); + }); + + it.each([ + ["never", null], + ["never", VaultTimeoutAction.Lock], + ["never", VaultTimeoutAction.LogOut], + ["immediately", null], + ["immediately", VaultTimeoutAction.Lock], + ["immediately", VaultTimeoutAction.LogOut], + ["onSystemLock", null], + ["onSystemLock", VaultTimeoutAction.Lock], + ["onSystemLock", VaultTimeoutAction.LogOut], + ["onAppRestart", null], + ["onAppRestart", VaultTimeoutAction.Lock], + ["onAppRestart", VaultTimeoutAction.LogOut], + ])( + "should return default 8 hours for backward compatibility when type is %s and action is %s", + (type, action) => { + component.data.patchValue({ + type: type as SessionTimeoutType, + hours: 5, + minutes: 25, + action: action as SessionTimeoutAction, + }); + + const result = component["buildRequestData"](); + + expect(result).toEqual({ + type, + minutes: 480, + action, + }); + }, + ); + }); +}); diff --git a/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout.component.ts b/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout.component.ts new file mode 100644 index 00000000000..3e40b9f0d80 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/key-management/policies/session-timeout.component.ts @@ -0,0 +1,197 @@ +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { FormBuilder, FormControl, Validators } from "@angular/forms"; +import { + BehaviorSubject, + concatMap, + firstValueFrom, + Subject, + takeUntil, + withLatestFrom, +} from "rxjs"; + +import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { VaultTimeoutAction } from "@bitwarden/common/key-management/vault-timeout"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogService } from "@bitwarden/components"; +import { + BasePolicyEditDefinition, + BasePolicyEditComponent, +} from "@bitwarden/web-vault/app/admin-console/organizations/policies"; +import { SharedModule } from "@bitwarden/web-vault/app/shared"; + +import { SessionTimeoutConfirmationNeverComponent } from "./session-timeout-confirmation-never.component"; + +export type SessionTimeoutAction = null | "lock" | "logOut"; +export type SessionTimeoutType = + | null + | "never" + | "onAppRestart" + | "onSystemLock" + | "immediately" + | "custom"; + +export class SessionTimeoutPolicy extends BasePolicyEditDefinition { + name = "sessionTimeoutPolicyTitle"; + description = "sessionTimeoutPolicyDescription"; + type = PolicyType.MaximumVaultTimeout; + component = SessionTimeoutPolicyComponent; +} + +const DEFAULT_HOURS = 8; +const DEFAULT_MINUTES = 0; + +@Component({ + templateUrl: "session-timeout.component.html", + imports: [SharedModule], +}) +export class SessionTimeoutPolicyComponent + extends BasePolicyEditComponent + implements OnInit, OnDestroy +{ + private destroy$ = new Subject<void>(); + private lastConfirmedType$ = new BehaviorSubject<SessionTimeoutType>(null); + + actionOptions: { name: string; value: SessionTimeoutAction }[]; + typeOptions: { name: string; value: SessionTimeoutType }[]; + data = this.formBuilder.group({ + type: new FormControl<SessionTimeoutType>(null, [Validators.required]), + hours: new FormControl<number>( + { + value: DEFAULT_HOURS, + disabled: true, + }, + [Validators.required], + ), + minutes: new FormControl<number>( + { + value: DEFAULT_MINUTES, + disabled: true, + }, + [Validators.required], + ), + action: new FormControl<SessionTimeoutAction>(null), + }); + + constructor( + private formBuilder: FormBuilder, + private i18nService: I18nService, + private dialogService: DialogService, + ) { + super(); + this.actionOptions = [ + { name: i18nService.t("userPreference"), value: null }, + { name: i18nService.t("lock"), value: VaultTimeoutAction.Lock }, + { name: i18nService.t("logOut"), value: VaultTimeoutAction.LogOut }, + ]; + this.typeOptions = [ + { name: i18nService.t("immediately"), value: "immediately" }, + { name: i18nService.t("custom"), value: "custom" }, + { name: i18nService.t("onSystemLock"), value: "onSystemLock" }, + { name: i18nService.t("onAppRestart"), value: "onAppRestart" }, + { name: i18nService.t("never"), value: "never" }, + ]; + } + + ngOnInit() { + super.ngOnInit(); + + const typeControl = this.data.controls.type; + this.lastConfirmedType$.next(typeControl.value ?? null); + + typeControl.valueChanges + .pipe( + withLatestFrom(this.lastConfirmedType$), + concatMap(async ([newType, lastConfirmedType]) => { + const confirmed = await this.confirmTypeChange(newType); + if (confirmed) { + this.updateFormControls(newType); + this.lastConfirmedType$.next(newType); + } else { + typeControl.setValue(lastConfirmedType, { emitEvent: false }); + } + }), + takeUntil(this.destroy$), + ) + .subscribe(); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + protected override loadData() { + const minutes: number | null = this.policyResponse?.data?.minutes ?? null; + const action: SessionTimeoutAction = + this.policyResponse?.data?.action ?? (null satisfies SessionTimeoutAction); + // For backward compatibility, the "type" field might not exist, hence we initialize it based on the presence of "minutes" + const type: SessionTimeoutType = + this.policyResponse?.data?.type ?? ((minutes ? "custom" : null) satisfies SessionTimeoutType); + + this.updateFormControls(type); + this.data.patchValue({ + type: type, + hours: minutes ? Math.floor(minutes / 60) : DEFAULT_HOURS, + minutes: minutes ? minutes % 60 : DEFAULT_MINUTES, + action: action, + }); + } + + protected override buildRequestData() { + this.data.markAllAsTouched(); + this.data.updateValueAndValidity(); + if (this.data.invalid) { + if (this.data.controls.type.hasError("required")) { + throw new Error(this.i18nService.t("maximumAllowedTimeoutRequired")); + } + throw new Error(this.i18nService.t("sessionTimeoutPolicyInvalidTime")); + } + + let minutes = this.data.value.hours! * 60 + this.data.value.minutes!; + + const type = this.data.value.type; + if (type === "custom") { + if (minutes <= 0) { + throw new Error(this.i18nService.t("sessionTimeoutPolicyInvalidTime")); + } + } else { + // For backwards compatibility, we set minutes to 8 hours, so older client's vault timeout will not be broken + minutes = DEFAULT_HOURS * 60 + DEFAULT_MINUTES; + } + + return { + type, + minutes, + action: this.data.value.action, + }; + } + + private async confirmTypeChange(newType: SessionTimeoutType): Promise<boolean> { + if (newType === "never") { + const dialogRef = SessionTimeoutConfirmationNeverComponent.open(this.dialogService); + return !!(await firstValueFrom(dialogRef.closed)); + } else if (newType === "onSystemLock") { + return await this.dialogService.openSimpleDialog({ + type: "info", + title: { key: "sessionTimeoutConfirmationOnSystemLockTitle" }, + content: { key: "sessionTimeoutConfirmationOnSystemLockDescription" }, + acceptButtonText: { key: "continue" }, + cancelButtonText: { key: "cancel" }, + }); + } + + return true; + } + + private updateFormControls(type: SessionTimeoutType) { + const hoursControl = this.data.controls.hours; + const minutesControl = this.data.controls.minutes; + if (type === "custom") { + hoursControl.enable(); + minutesControl.enable(); + } else { + hoursControl.disable(); + minutesControl.disable(); + } + } +}