From 1e7d54f7fb3b6ced43ec01e36f27424faa3ed99f Mon Sep 17 00:00:00 2001 From: Alec Rippberger <127791530+alec-livefront@users.noreply.github.com> Date: Thu, 10 Apr 2025 14:55:26 -0500 Subject: [PATCH 01/19] fix(auth): [PM-1779] replace wildcard with window.location.origin in postMessage Improve security by using specific origin instead of wildcard in postMessage calls to prevent potential information leakage to third parties. PM-1779 --- apps/web/src/connectors/duo-redirect.ts | 2 +- apps/web/src/connectors/sso.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/connectors/duo-redirect.ts b/apps/web/src/connectors/duo-redirect.ts index b5300ff65e7..c19e056d306 100644 --- a/apps/web/src/connectors/duo-redirect.ts +++ b/apps/web/src/connectors/duo-redirect.ts @@ -33,7 +33,7 @@ window.addEventListener("load", async () => { displayHandoffMessage(client); } else if (client === "browser") { - window.postMessage({ command: "duoResult", code: code, state: state }, "*"); + window.postMessage({ command: "duoResult", code, state }, window.location.origin); displayHandoffMessage(client); } else if (client === "mobile" || client === "desktop") { if (client === "desktop") { diff --git a/apps/web/src/connectors/sso.ts b/apps/web/src/connectors/sso.ts index 4fdab71be3b..886742c4c49 100644 --- a/apps/web/src/connectors/sso.ts +++ b/apps/web/src/connectors/sso.ts @@ -32,7 +32,7 @@ function initiateWebAppSso(code: string, state: string) { } function initiateBrowserSso(code: string, state: string, lastpass: boolean) { - window.postMessage({ command: "authResult", code: code, state: state, lastpass: lastpass }, "*"); + window.postMessage({ command: "authResult", code, state, lastpass }, window.location.origin); const handOffMessage = ("; " + document.cookie) .split("; ssoHandOffMessage=") .pop() From e88813e983ca28f9b5be10eed089ba70e03984f2 Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Thu, 10 Apr 2025 18:01:28 -0400 Subject: [PATCH 02/19] remove duplicate message catalog keys (#14228) --- apps/web/src/locales/en/messages.json | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 8fe74a5a2d2..0193fc4862b 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -149,27 +149,6 @@ } } }, - "atRiskMembersWithCount": { - "message": "At-risk members ($COUNT$)", - "placeholders": { - "count": { - "content": "$1", - "example": "3" - } - } - }, - "atRiskMembersDescription": { - "message": "These members are logging into applications with weak, exposed, or reused passwords." - }, - "atRiskMembersDescriptionWithApp": { - "message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.", - "placeholders": { - "appname": { - "content": "$1", - "example": "Salesforce" - } - } - }, "totalMembers": { "message": "Total members" }, @@ -9314,9 +9293,6 @@ "sdksDesc": { "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." }, - "singleSignOn": { - "message": "Single sign-on" - }, "ssoDescStart": { "message": "Configure", "description": "This represents the beginning of a sentence, broken up to include links. The full sentence will be 'Configure single sign-on for Bitwarden using the implementation guide for your Identity Provider." From f7934b98c6000ef1dc023549835fbdad527955d1 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Thu, 10 Apr 2025 18:58:49 -0400 Subject: [PATCH 03/19] fix(login): [PM-11502] Support Remember Email option consistently * Moved saving of SSO email outside of browser/desktop code * Clarified comments. * Tests * Refactored login component services to manage state * Fixed input on login component * Fixed tests * Linting * Moved web setting in state into web override * updated tests * Fixed typing. * Fixed type safety issues. * Added comments and renamed for clarity. * Removed method parameters that weren't used * Added clarifying comments * Added more comments. * Removed test that is not necessary on base * Test cleanup * More comments. * Linting * Fixed test. * Fixed base URL * Fixed typechecking. * Type checking * Moved setting of email state to default service * Added comments. * Consolidated SSO URL formatting * Updated comment * Fixed reference. * Fixed missing parameter. * Initialized service. * Added comments * Added initialization of new service * Made email optional due to CLI. * Fixed comment on handleSsoClick. * Added SSO email persistence to v1 component. * Updated login email service. * Updated setting of remember me * Removed unnecessary input checking and rearranged functions * Fixed name * Added handling of Remember Email to old component for passkey click * Updated v1 component to persist the email on Continue click * Fix merge conflicts. * Merge conflicts in login component. * Persisted login email on v1 browser component. * Merge conflicts * fix(snap) [PM-17464][PM-17463][PM-15587] Allow Snap to use custom callback protocol * Removed Snap from custom protocol workaround * Fixed tests. * Updated case numbers on test * Resolved PR feedback. * PM-11502 - LoginEmailSvcAbstraction - mark methods as abstract to satisfy strict ts. * Removed test * Changed to persist on leaving fields instead of button click. * Fixed type checking. --------- Co-authored-by: Bernd Schoolmann Co-authored-by: Jared Snider Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> --- .../desktop-login-component.service.spec.ts | 21 +- .../login/desktop-login-component.service.ts | 2 +- .../src/services/jslib-services.module.ts | 2 +- .../login-via-auth-request.component.ts | 5 - .../src/angular/login/login.component.html | 19 +- .../auth/src/angular/login/login.component.ts | 255 ++++++++++-------- .../new-device-verification.component.ts | 10 +- .../two-factor-auth.component.spec.ts | 13 - .../two-factor-auth.component.ts | 1 - .../abstractions/login-email.service.ts | 37 +-- .../login-email/login-email.service.spec.ts | 110 +++----- .../login-email/login-email.service.ts | 58 ++-- .../default-login-success-handler.service.ts | 3 + 13 files changed, 241 insertions(+), 295 deletions(-) diff --git a/apps/desktop/src/auth/login/desktop-login-component.service.spec.ts b/apps/desktop/src/auth/login/desktop-login-component.service.spec.ts index 94bc73d2e93..c88627250c9 100644 --- a/apps/desktop/src/auth/login/desktop-login-component.service.spec.ts +++ b/apps/desktop/src/auth/login/desktop-login-component.service.spec.ts @@ -107,22 +107,17 @@ describe("DesktopLoginComponentService", () => { }); describe("redirectToSso", () => { - // Array of all permutations of isAppImage, isSnapStore, and isDev + // Array of all permutations of isAppImage and isDev const permutations = [ - [true, false, false], // Case 1: isAppImage true - [false, true, false], // Case 2: isSnapStore true - [false, false, true], // Case 3: isDev true - [true, true, false], // Case 4: isAppImage and isSnapStore true - [true, false, true], // Case 5: isAppImage and isDev true - [false, true, true], // Case 6: isSnapStore and isDev true - [true, true, true], // Case 7: all true - [false, false, false], // Case 8: all false + [true, false], // Case 1: isAppImage true + [false, true], // Case 2: isDev true + [true, true], // Case 3: all true + [false, false], // Case 4: all false ]; - permutations.forEach(([isAppImage, isSnapStore, isDev]) => { - it(`executes correct logic for isAppImage=${isAppImage}, isSnapStore=${isSnapStore}, isDev=${isDev}`, async () => { + permutations.forEach(([isAppImage, isDev]) => { + it(`executes correct logic for isAppImage=${isAppImage}, isDev=${isDev}`, async () => { (global as any).ipc.platform.isAppImage = isAppImage; - (global as any).ipc.platform.isSnapStore = isSnapStore; (global as any).ipc.platform.isDev = isDev; const email = "test@bitwarden.com"; @@ -136,7 +131,7 @@ describe("DesktopLoginComponentService", () => { await service.redirectToSsoLogin(email); - if (isAppImage || isSnapStore || isDev) { + if (isAppImage || isDev) { expect(ipc.platform.localhostCallbackService.openSsoPrompt).toHaveBeenCalledWith( codeChallenge, state, diff --git a/apps/desktop/src/auth/login/desktop-login-component.service.ts b/apps/desktop/src/auth/login/desktop-login-component.service.ts index 7341e0fe039..60e7791b384 100644 --- a/apps/desktop/src/auth/login/desktop-login-component.service.ts +++ b/apps/desktop/src/auth/login/desktop-login-component.service.ts @@ -51,7 +51,7 @@ export class DesktopLoginComponentService ): Promise { // For platforms that cannot support a protocol-based (e.g. bitwarden://) callback, we use a localhost callback // Otherwise, we launch the SSO component in a browser window and wait for the callback - if (ipc.platform.isAppImage || ipc.platform.isSnapStore || ipc.platform.isDev) { + if (ipc.platform.isAppImage || ipc.platform.isDev) { await this.initiateSsoThroughLocalhostCallback(email, state, codeChallenge); } else { const env = await firstValueFrom(this.environmentService.environment$); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 2caad95811b..3cce9b5357e 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1470,7 +1470,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: LoginSuccessHandlerService, useClass: DefaultLoginSuccessHandlerService, - deps: [SyncService, UserAsymmetricKeysRegenerationService], + deps: [SyncService, UserAsymmetricKeysRegenerationService, LoginEmailService], }), safeProvider({ provide: TaskService, 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 cb4b7bc4c8c..0af52e02b84 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 @@ -809,11 +809,6 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { } private async handleSuccessfulLoginNavigation(userId: UserId) { - if (this.flow === Flow.StandardAuthRequest) { - // Only need to set remembered email on standard login with auth req flow - await this.loginEmailService.saveEmailSettings(); - } - await this.loginSuccessHandlerService.run(userId); await this.router.navigate(["vault"]); } diff --git a/libs/auth/src/angular/login/login.component.html b/libs/auth/src/angular/login/login.component.html index b04a54da425..35ef1fa9b50 100644 --- a/libs/auth/src/angular/login/login.component.html +++ b/libs/auth/src/angular/login/login.component.html @@ -27,7 +27,12 @@ - + {{ "rememberEmail" | i18n }} @@ -39,18 +44,18 @@
{{ "or" | i18n }}
- + - {{ "logInWithPasskey" | i18n }} - + diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts index cc38ec5dfb3..55c282be55c 100644 --- a/libs/auth/src/angular/login/login.component.ts +++ b/libs/auth/src/angular/login/login.component.ts @@ -148,6 +148,62 @@ export class LoginComponent implements OnInit, OnDestroy { this.destroy$.complete(); } + private async defaultOnInit(): Promise { + let paramEmailIsSet = false; + + const params = await firstValueFrom(this.activatedRoute.queryParams); + + if (params) { + const qParamsEmail = params.email; + + // If there is an email in the query params, set that email as the form field value + if (qParamsEmail != null && qParamsEmail.indexOf("@") > -1) { + this.formGroup.controls.email.setValue(qParamsEmail); + paramEmailIsSet = true; + } + } + + // If there are no params or no email in the query params, loadEmailSettings from state + if (!paramEmailIsSet) { + await this.loadRememberedEmail(); + } + + // Check to see if the device is known so that we can show the Login with Device option + if (this.emailFormControl.value) { + await this.getKnownDevice(this.emailFormControl.value); + } + + // Backup check to handle unknown case where activatedRoute is not available + // This shouldn't happen under normal circumstances + if (!this.activatedRoute) { + await this.loadRememberedEmail(); + } + } + + private async desktopOnInit(): Promise { + // TODO: refactor to not use deprecated broadcaster service. + this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => { + this.ngZone.run(() => { + switch (message.command) { + case "windowIsFocused": + if (this.deferFocus === null) { + this.deferFocus = !message.windowIsFocused; + if (!this.deferFocus) { + this.focusInput(); + } + } else if (this.deferFocus && message.windowIsFocused) { + this.focusInput(); + this.deferFocus = false; + } + break; + default: + } + }); + }); + + this.messagingService.send("getWindowIsFocused"); + } + submit = async (): Promise => { if (this.clientType === ClientType.Desktop) { if (this.loginUiState !== LoginUiState.MASTER_PASSWORD_ENTRY) { @@ -172,7 +228,6 @@ export class LoginComponent implements OnInit, OnDestroy { try { const authResult = await this.loginStrategyService.logIn(credentials); - await this.saveEmailSettings(); await this.handleAuthResult(authResult); } catch (error) { this.logService.error(error); @@ -250,7 +305,6 @@ export class LoginComponent implements OnInit, OnDestroy { // User logged in successfully so execute side effects await this.loginSuccessHandlerService.run(authResult.userId); - this.loginEmailService.clearValues(); // Determine where to send the user next if (authResult.forcePasswordReset != ForceSetPasswordReason.None) { @@ -288,7 +342,6 @@ export class LoginComponent implements OnInit, OnDestroy { await this.router.navigate(["vault"]); } } - /** * Checks if the master password meets the enforced policy requirements * and if the user is required to change their password. @@ -344,11 +397,10 @@ export class LoginComponent implements OnInit, OnDestroy { return; } - await this.saveEmailSettings(); await this.router.navigate(["/login-with-device"]); } - protected async validateEmail(): Promise { + protected async emailIsValid(): Promise { this.formGroup.controls.email.markAsTouched(); this.formGroup.controls.email.updateValueAndValidity({ onlySelf: true, emitEvent: true }); return this.formGroup.controls.email.valid; @@ -399,37 +451,14 @@ export class LoginComponent implements OnInit, OnDestroy { } } - /** - * Set the email value from the input field. - * @param event The event object from the input field. - */ - onEmailInput(event: Event) { - const emailInput = event.target as HTMLInputElement; - this.formGroup.controls.email.setValue(emailInput.value); - this.loginEmailService.setLoginEmail(emailInput.value); - } - isLoginWithPasskeySupported() { return this.loginComponentService.isLoginWithPasskeySupported(); } protected async goToHint(): Promise { - await this.saveEmailSettings(); await this.router.navigateByUrl("/hint"); } - protected async saveEmailSettings(): Promise { - const email = this.formGroup.value.email; - if (!email) { - this.logService.error("Email is required to save email settings."); - return; - } - - await this.loginEmailService.setLoginEmail(email); - this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail ?? false); - await this.loginEmailService.saveEmailSettings(); - } - /** * Continue button clicked (or enter key pressed). * Adds the login url to the browser's history so that the back button can be used to go back to the email entry state. @@ -445,13 +474,44 @@ export class LoginComponent implements OnInit, OnDestroy { * Continue to the master password entry state (only if email is validated) */ protected async continue(): Promise { - const isEmailValid = await this.validateEmail(); + const isEmailValid = await this.emailIsValid(); if (isEmailValid) { await this.toggleLoginUiState(LoginUiState.MASTER_PASSWORD_ENTRY); } } + /** + * Handle the Login with Passkey button click. + * We need a handler here in order to persist the remember email selection to state before routing. + * @param event - The event object. + */ + async handleLoginWithPasskeyClick() { + await this.router.navigate(["/login-with-passkey"]); + } + + /** + * Handle the SSO button click. + * @param event - The event object. + */ + async handleSsoClick() { + // Make sure the email is valid + const isEmailValid = await this.emailIsValid(); + if (!isEmailValid) { + return; + } + + // Make sure the email is not empty, for type safety + const email = this.formGroup.value.email; + if (!email) { + this.logService.error("Email is required for SSO"); + return; + } + + // Send the user to SSO, either through routing or through redirecting to the web app + await this.loginComponentService.redirectToSsoLogin(email); + } + /** * Call to check if the device is known. * Known means that the user has logged in with this device before. @@ -473,23 +533,17 @@ export class LoginComponent implements OnInit, OnDestroy { } } - private async loadEmailSettings(): Promise { - // Try to load the email from memory first - const email = await firstValueFrom(this.loginEmailService.loginEmail$); - const rememberEmail = this.loginEmailService.getRememberEmail(); - - if (email) { - this.formGroup.controls.email.setValue(email); - this.formGroup.controls.rememberEmail.setValue(rememberEmail); + /** + * Check to see if the user has remembered an email on the current device. + * If so, set the email in the form field and set rememberEmail to true. If not, set rememberEmail to false. + */ + private async loadRememberedEmail(): Promise { + const storedEmail = await firstValueFrom(this.loginEmailService.rememberedEmail$); + if (storedEmail) { + this.formGroup.controls.email.setValue(storedEmail); + this.formGroup.controls.rememberEmail.setValue(true); } else { - // If there is no email in memory, check for a storedEmail on disk - const storedEmail = await firstValueFrom(this.loginEmailService.storedEmail$); - - if (storedEmail) { - this.formGroup.controls.email.setValue(storedEmail); - // If there is a storedEmail, rememberEmail defaults to true - this.formGroup.controls.rememberEmail.setValue(true); - } + this.formGroup.controls.rememberEmail.setValue(false); } } @@ -503,62 +557,6 @@ export class LoginComponent implements OnInit, OnDestroy { ?.focus(); } - private async defaultOnInit(): Promise { - let paramEmailIsSet = false; - - const params = await firstValueFrom(this.activatedRoute.queryParams); - - if (params) { - const qParamsEmail = params.email; - - // If there is an email in the query params, set that email as the form field value - if (qParamsEmail != null && qParamsEmail.indexOf("@") > -1) { - this.formGroup.controls.email.setValue(qParamsEmail); - paramEmailIsSet = true; - } - } - - // If there are no params or no email in the query params, loadEmailSettings from state - if (!paramEmailIsSet) { - await this.loadEmailSettings(); - } - - // Check to see if the device is known so that we can show the Login with Device option - if (this.emailFormControl.value) { - await this.getKnownDevice(this.emailFormControl.value); - } - - // Backup check to handle unknown case where activatedRoute is not available - // This shouldn't happen under normal circumstances - if (!this.activatedRoute) { - await this.loadEmailSettings(); - } - } - - private async desktopOnInit(): Promise { - // TODO: refactor to not use deprecated broadcaster service. - this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => { - this.ngZone.run(() => { - switch (message.command) { - case "windowIsFocused": - if (this.deferFocus === null) { - this.deferFocus = !message.windowIsFocused; - if (!this.deferFocus) { - this.focusInput(); - } - } else if (this.deferFocus && message.windowIsFocused) { - this.focusInput(); - this.deferFocus = false; - } - break; - default: - } - }); - }); - - this.messagingService.send("getWindowIsFocused"); - } - /** * Helper function to determine if the back button should be shown. * @returns true if the back button should be shown. @@ -597,27 +595,46 @@ export class LoginComponent implements OnInit, OnDestroy { }; /** - * Handle the SSO button click. + * Persist the entered email address and the user's choice to remember it to state. */ - async handleSsoClick() { - const email = this.formGroup.value.email; - - // Make sure the email is valid - const isEmailValid = await this.validateEmail(); - if (!isEmailValid) { - return; + private async persistEmailIfValid(): Promise { + if (await this.emailIsValid()) { + const email = this.formGroup.value.email; + const rememberEmail = this.formGroup.value.rememberEmail ?? false; + if (!email) { + return; + } + await this.loginEmailService.setLoginEmail(email); + await this.loginEmailService.setRememberedEmailChoice(email, rememberEmail); + } else { + await this.loginEmailService.clearLoginEmail(); + await this.loginEmailService.clearRememberedEmail(); } + } - // Make sure the email is not empty, for type safety - if (!email) { - this.logService.error("Email is required for SSO"); - return; - } + /** + * Set the email value from the input field. + * We only update the form controls onSubmit instead of onBlur because we don't want to show validation errors until + * the user submits. This is because currently our validation errors are shown below the input fields, and + * displaying them causes the screen to "jump". + * @param event The event object from the input field. + */ + async onEmailInput(event: Event) { + const emailInput = event.target as HTMLInputElement; + this.formGroup.controls.email.setValue(emailInput.value); + await this.persistEmailIfValid(); + } - // Save the email configuration for the login component - await this.saveEmailSettings(); - - // Send the user to SSO, either through routing or through redirecting to the web app - await this.loginComponentService.redirectToSsoLogin(email); + /** + * Set the Remember Email value from the input field. + * We only update the form controls onSubmit instead of onBlur because we don't want to show validation errors until + * the user submits. This is because currently our validation errors are shown below the input fields, and + * displaying them causes the screen to "jump". + * @param event The event object from the input field. + */ + async onRememberEmailInput(event: Event) { + const rememberEmailInput = event.target as HTMLInputElement; + this.formGroup.controls.rememberEmail.setValue(rememberEmailInput.checked); + await this.persistEmailIfValid(); } } 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 57583eb24d2..c083643c9b4 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 @@ -5,10 +5,10 @@ import { Router } from "@angular/router"; import { Subject, takeUntil } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { LoginSuccessHandlerService } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { AsyncActionsModule, ButtonModule, @@ -17,7 +17,6 @@ import { LinkModule, } from "@bitwarden/components"; -import { LoginEmailServiceAbstraction } from "../../common/abstractions/login-email.service"; import { LoginStrategyServiceAbstraction } from "../../common/abstractions/login-strategy.service"; /** @@ -60,8 +59,7 @@ export class NewDeviceVerificationComponent implements OnInit, OnDestroy { private loginStrategyService: LoginStrategyServiceAbstraction, private logService: LogService, private i18nService: I18nService, - private syncService: SyncService, - private loginEmailService: LoginEmailServiceAbstraction, + private loginSuccessHandlerService: LoginSuccessHandlerService, ) {} async ngOnInit() { @@ -143,9 +141,7 @@ export class NewDeviceVerificationComponent implements OnInit, OnDestroy { return; } - this.loginEmailService.clearValues(); - - await this.syncService.fullSync(true); + this.loginSuccessHandlerService.run(authResult.userId); // If verification succeeds, navigate to vault await this.router.navigate(["/vault"]); 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 20e9aa73048..8378ac7794c 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 @@ -254,19 +254,6 @@ describe("TwoFactorAuthComponent", () => { ); }); - it("calls loginEmailService.clearValues() when login is successful", async () => { - // Arrange - mockLoginStrategyService.logInTwoFactor.mockResolvedValue(new AuthResult()); - // spy on loginEmailService.clearValues - const clearValuesSpy = jest.spyOn(mockLoginEmailService, "clearValues"); - - // Act - await component.submit(token, remember); - - // Assert - expect(clearValuesSpy).toHaveBeenCalled(); - }); - describe("Set Master Password scenarios", () => { beforeEach(() => { const authResult = new AuthResult(); 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 74b5db634f5..6cdf42b76da 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 @@ -382,7 +382,6 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { // User is fully logged in so handle any post login logic before executing navigation await this.loginSuccessHandlerService.run(authResult.userId); - this.loginEmailService.clearValues(); // Save off the OrgSsoIdentifier for use in the TDE flows // - TDE login decryption options component diff --git a/libs/auth/src/common/abstractions/login-email.service.ts b/libs/auth/src/common/abstractions/login-email.service.ts index fc72c4cd262..4d575170362 100644 --- a/libs/auth/src/common/abstractions/login-email.service.ts +++ b/libs/auth/src/common/abstractions/login-email.service.ts @@ -1,43 +1,34 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; export abstract class LoginEmailServiceAbstraction { /** - * An observable that monitors the loginEmail in memory. + * An observable that monitors the loginEmail. * The loginEmail is the email that is being used in the current login process. */ - loginEmail$: Observable; + abstract loginEmail$: Observable; /** - * An observable that monitors the storedEmail on disk. + * An observable that monitors the remembered email. * This will return null if an account is being added. */ - storedEmail$: Observable; + abstract rememberedEmail$: Observable; /** * Sets the loginEmail in memory. * The loginEmail is the email that is being used in the current login process. + * Consumed through `loginEmail$` observable. */ - setLoginEmail: (email: string) => Promise; + abstract setLoginEmail: (email: string) => Promise; /** - * Gets from memory whether or not the email should be stored on disk when `saveEmailSettings` is called. - * @returns A boolean stating whether or not the email should be stored on disk. + * Persist the user's choice of whether to remember their email on subsequent login attempts. + * Consumed through `rememberedEmail$` observable. */ - getRememberEmail: () => boolean; + abstract setRememberedEmailChoice: (email: string, remember: boolean) => Promise; /** - * Sets in memory whether or not the email should be stored on disk when `saveEmailSettings` is called. + * Clears the in-progress login email, to be used after a successful login. */ - setRememberEmail: (value: boolean) => void; + abstract clearLoginEmail: () => Promise; + /** - * Sets the email and rememberEmail properties in memory to null. + * Clears the remembered email. */ - clearValues: () => void; - /** - * Saves or clears the email on disk - * - If an account is being added, only changes the stored email when rememberEmail is true. - * - If rememberEmail is true, sets the email on disk to the current email. - * - If rememberEmail is false, sets the email on disk to null. - * Always clears the email and rememberEmail properties from memory. - * @returns A promise that resolves once the email settings are saved. - */ - saveEmailSettings: () => Promise; + abstract clearRememberedEmail: () => Promise; } diff --git a/libs/auth/src/common/services/login-email/login-email.service.spec.ts b/libs/auth/src/common/services/login-email/login-email.service.spec.ts index 8bb9b962eaf..819c9d496b9 100644 --- a/libs/auth/src/common/services/login-email/login-email.service.spec.ts +++ b/libs/auth/src/common/services/login-email/login-email.service.spec.ts @@ -14,7 +14,7 @@ import { UserId } from "@bitwarden/common/types/guid"; import { LoginEmailService, STORED_EMAIL } from "./login-email.service"; describe("LoginEmailService", () => { - let sut: LoginEmailService; + let service: LoginEmailService; let accountService: FakeAccountService; let authService: MockProxy; @@ -34,119 +34,93 @@ describe("LoginEmailService", () => { mockAuthStatuses$ = new BehaviorSubject>({}); authService.authStatuses$ = mockAuthStatuses$; - sut = new LoginEmailService(accountService, authService, stateProvider); + service = new LoginEmailService(accountService, authService, stateProvider); }); afterEach(() => { jest.clearAllMocks(); }); - describe("storedEmail$", () => { - it("returns the stored email when not adding an account", async () => { - await sut.setLoginEmail("userEmail@bitwarden.com"); - sut.setRememberEmail(true); - await sut.saveEmailSettings(); + describe("rememberedEmail$", () => { + it("returns the remembered email when not adding an account", async () => { + const testEmail = "test@bitwarden.com"; - const result = await firstValueFrom(sut.storedEmail$); + await service.setRememberedEmailChoice(testEmail, true); - expect(result).toEqual("userEmail@bitwarden.com"); + const result = await firstValueFrom(service.rememberedEmail$); + + expect(result).toEqual(testEmail); }); - it("returns the stored email when not adding an account and the user has just logged in", async () => { - await sut.setLoginEmail("userEmail@bitwarden.com"); - sut.setRememberEmail(true); - await sut.saveEmailSettings(); + it("returns the remembered email when not adding an account and the user has just logged in", async () => { + const testEmail = "test@bitwarden.com"; + + await service.setRememberedEmailChoice(testEmail, true); mockAuthStatuses$.next({ [userId]: AuthenticationStatus.Unlocked }); // account service already initialized with userId as active user - const result = await firstValueFrom(sut.storedEmail$); + const result = await firstValueFrom(service.rememberedEmail$); - expect(result).toEqual("userEmail@bitwarden.com"); + expect(result).toEqual(testEmail); }); it("returns null when adding an account", async () => { - await sut.setLoginEmail("userEmail@bitwarden.com"); - sut.setRememberEmail(true); - await sut.saveEmailSettings(); + const testEmail = "test@bitwarden.com"; + + await service.setRememberedEmailChoice(testEmail, true); mockAuthStatuses$.next({ [userId]: AuthenticationStatus.Unlocked, ["OtherUserId" as UserId]: AuthenticationStatus.Locked, }); - const result = await firstValueFrom(sut.storedEmail$); + const result = await firstValueFrom(service.rememberedEmail$); expect(result).toBeNull(); }); }); - describe("saveEmailSettings", () => { - it("saves the email when not adding an account", async () => { - await sut.setLoginEmail("userEmail@bitwarden.com"); - sut.setRememberEmail(true); - await sut.saveEmailSettings(); + describe("setRememberedEmailChoice", () => { + it("sets the remembered email when remember is true", async () => { + const testEmail = "test@bitwarden.com"; + + await service.setRememberedEmailChoice(testEmail, true); const result = await firstValueFrom(storedEmailState.state$); - expect(result).toEqual("userEmail@bitwarden.com"); + expect(result).toEqual(testEmail); }); - it("clears the email when not adding an account and rememberEmail is false", async () => { + it("clears the remembered email when remember is false", async () => { storedEmailState.stateSubject.next("initialEmail@bitwarden.com"); - await sut.setLoginEmail("userEmail@bitwarden.com"); - sut.setRememberEmail(false); - await sut.saveEmailSettings(); + const testEmail = "test@bitwarden.com"; + + await service.setRememberedEmailChoice(testEmail, false); const result = await firstValueFrom(storedEmailState.state$); expect(result).toBeNull(); }); + }); - it("saves the email when adding an account", async () => { - mockAuthStatuses$.next({ - [userId]: AuthenticationStatus.Unlocked, - ["OtherUserId" as UserId]: AuthenticationStatus.Locked, - }); + describe("setLoginEmail", () => { + it("sets the login email", async () => { + const testEmail = "test@bitwarden.com"; + await service.setLoginEmail(testEmail); - await sut.setLoginEmail("userEmail@bitwarden.com"); - sut.setRememberEmail(true); - await sut.saveEmailSettings(); - - const result = await firstValueFrom(storedEmailState.state$); - - expect(result).toEqual("userEmail@bitwarden.com"); + expect(await firstValueFrom(service.loginEmail$)).toEqual(testEmail); }); + }); - it("does not clear the email when adding an account and rememberEmail is false", async () => { - storedEmailState.stateSubject.next("initialEmail@bitwarden.com"); + describe("clearLoginEmail", () => { + it("clears the login email", async () => { + const testEmail = "test@bitwarden.com"; + await service.setLoginEmail(testEmail); + await service.clearLoginEmail(); - mockAuthStatuses$.next({ - [userId]: AuthenticationStatus.Unlocked, - ["OtherUserId" as UserId]: AuthenticationStatus.Locked, - }); - - await sut.setLoginEmail("userEmail@bitwarden.com"); - sut.setRememberEmail(false); - await sut.saveEmailSettings(); - - const result = await firstValueFrom(storedEmailState.state$); - - // result should not be null - expect(result).toEqual("initialEmail@bitwarden.com"); - }); - - it("does not clear the email and rememberEmail after saving", async () => { - // Browser uses these values to maintain the email between login and 2fa components so - // we do not want to clear them too early. - await sut.setLoginEmail("userEmail@bitwarden.com"); - sut.setRememberEmail(true); - await sut.saveEmailSettings(); - - const result = await firstValueFrom(sut.loginEmail$); - - expect(result).toBe("userEmail@bitwarden.com"); + expect(await firstValueFrom(service.loginEmail$)).toBeNull(); }); }); }); diff --git a/libs/auth/src/common/services/login-email/login-email.service.ts b/libs/auth/src/common/services/login-email/login-email.service.ts index aa13afd5004..6ca817772b0 100644 --- a/libs/auth/src/common/services/login-email/login-email.service.ts +++ b/libs/auth/src/common/services/login-email/login-email.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable, firstValueFrom, switchMap } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -26,8 +24,6 @@ export const STORED_EMAIL = new KeyDefinition(LOGIN_EMAIL_DISK, "storedE }); export class LoginEmailService implements LoginEmailServiceAbstraction { - private rememberEmail: boolean; - // True if an account is currently being added through account switching private readonly addingAccount$: Observable; @@ -35,7 +31,7 @@ export class LoginEmailService implements LoginEmailServiceAbstraction { loginEmail$: Observable; private readonly storedEmailState: GlobalState; - storedEmail$: Observable; + rememberedEmail$: Observable; constructor( private accountService: AccountService, @@ -60,7 +56,7 @@ export class LoginEmailService implements LoginEmailServiceAbstraction { this.loginEmail$ = this.loginEmailState.state$; - this.storedEmail$ = this.storedEmailState.state$.pipe( + this.rememberedEmail$ = this.storedEmailState.state$.pipe( switchMap(async (storedEmail) => { // When adding an account, we don't show the stored email if (await firstValueFrom(this.addingAccount$)) { @@ -71,44 +67,32 @@ export class LoginEmailService implements LoginEmailServiceAbstraction { ); } + /** Sets the login email in memory. + * The login email is the email that is being used in the current login process. + */ async setLoginEmail(email: string) { await this.loginEmailState.update((_) => email); } - getRememberEmail() { - return this.rememberEmail; + /** + * Clears the in-progress login email from state. + * Note: Only clear on successful login or you are sure they are not needed. + * The extension client uses these values to maintain the email between login and 2fa components so + * we do not want to clear them too early. + */ + async clearLoginEmail() { + await this.loginEmailState.update((_) => null); } - setRememberEmail(value: boolean) { - this.rememberEmail = value ?? false; + async setRememberedEmailChoice(email: string, remember: boolean): Promise { + if (remember) { + await this.storedEmailState.update((_) => email); + } else { + await this.storedEmailState.update((_) => null); + } } - // Note: only clear values on successful login or you are sure they are not needed. - // Browser uses these values to maintain the email between login and 2fa components so - // we do not want to clear them too early. - async clearValues() { - await this.setLoginEmail(null); - this.rememberEmail = false; - } - - async saveEmailSettings() { - const addingAccount = await firstValueFrom(this.addingAccount$); - const email = await firstValueFrom(this.loginEmail$); - - await this.storedEmailState.update((storedEmail) => { - // If we're adding an account, only overwrite the stored email when rememberEmail is true - if (addingAccount) { - if (this.rememberEmail) { - return email; - } - return storedEmail; - } - - // Saving with rememberEmail set to false will clear the stored email - if (this.rememberEmail) { - return email; - } - return null; - }); + async clearRememberedEmail(): Promise { + await this.storedEmailState.update((_) => null); } } diff --git a/libs/auth/src/common/services/login-success-handler/default-login-success-handler.service.ts b/libs/auth/src/common/services/login-success-handler/default-login-success-handler.service.ts index 215329051df..70d56a2ad6c 100644 --- a/libs/auth/src/common/services/login-success-handler/default-login-success-handler.service.ts +++ b/libs/auth/src/common/services/login-success-handler/default-login-success-handler.service.ts @@ -3,14 +3,17 @@ import { UserId } from "@bitwarden/common/types/guid"; import { UserAsymmetricKeysRegenerationService } from "@bitwarden/key-management"; import { LoginSuccessHandlerService } from "../../abstractions/login-success-handler.service"; +import { LoginEmailService } from "../login-email/login-email.service"; export class DefaultLoginSuccessHandlerService implements LoginSuccessHandlerService { constructor( private syncService: SyncService, private userAsymmetricKeysRegenerationService: UserAsymmetricKeysRegenerationService, + private loginEmailService: LoginEmailService, ) {} async run(userId: UserId): Promise { await this.syncService.fullSync(true); await this.userAsymmetricKeysRegenerationService.regenerateIfNeeded(userId); + await this.loginEmailService.clearLoginEmail(); } } From c05e3df2e4608765362002f41eaf6c502e34475f Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Fri, 11 Apr 2025 10:08:56 +0200 Subject: [PATCH 04/19] Autosync the updated translations (#14232) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/af/messages.json | 3 +++ apps/desktop/src/locales/ar/messages.json | 3 +++ apps/desktop/src/locales/az/messages.json | 3 +++ apps/desktop/src/locales/be/messages.json | 3 +++ apps/desktop/src/locales/bg/messages.json | 3 +++ apps/desktop/src/locales/bn/messages.json | 3 +++ apps/desktop/src/locales/bs/messages.json | 3 +++ apps/desktop/src/locales/ca/messages.json | 7 +++++-- apps/desktop/src/locales/cs/messages.json | 3 +++ apps/desktop/src/locales/cy/messages.json | 3 +++ apps/desktop/src/locales/da/messages.json | 3 +++ apps/desktop/src/locales/de/messages.json | 3 +++ apps/desktop/src/locales/el/messages.json | 3 +++ apps/desktop/src/locales/en_GB/messages.json | 3 +++ apps/desktop/src/locales/en_IN/messages.json | 3 +++ apps/desktop/src/locales/eo/messages.json | 3 +++ apps/desktop/src/locales/es/messages.json | 3 +++ apps/desktop/src/locales/et/messages.json | 3 +++ apps/desktop/src/locales/eu/messages.json | 3 +++ apps/desktop/src/locales/fa/messages.json | 3 +++ apps/desktop/src/locales/fi/messages.json | 3 +++ apps/desktop/src/locales/fil/messages.json | 3 +++ apps/desktop/src/locales/fr/messages.json | 3 +++ apps/desktop/src/locales/gl/messages.json | 3 +++ apps/desktop/src/locales/he/messages.json | 3 +++ apps/desktop/src/locales/hi/messages.json | 3 +++ apps/desktop/src/locales/hr/messages.json | 3 +++ apps/desktop/src/locales/hu/messages.json | 3 +++ apps/desktop/src/locales/id/messages.json | 3 +++ apps/desktop/src/locales/it/messages.json | 3 +++ apps/desktop/src/locales/ja/messages.json | 13 ++++++++----- apps/desktop/src/locales/ka/messages.json | 3 +++ apps/desktop/src/locales/km/messages.json | 3 +++ apps/desktop/src/locales/kn/messages.json | 3 +++ apps/desktop/src/locales/ko/messages.json | 3 +++ apps/desktop/src/locales/lt/messages.json | 3 +++ apps/desktop/src/locales/lv/messages.json | 7 +++++-- apps/desktop/src/locales/me/messages.json | 3 +++ apps/desktop/src/locales/ml/messages.json | 3 +++ apps/desktop/src/locales/mr/messages.json | 3 +++ apps/desktop/src/locales/my/messages.json | 3 +++ apps/desktop/src/locales/nb/messages.json | 3 +++ apps/desktop/src/locales/ne/messages.json | 3 +++ apps/desktop/src/locales/nl/messages.json | 3 +++ apps/desktop/src/locales/nn/messages.json | 3 +++ apps/desktop/src/locales/or/messages.json | 3 +++ apps/desktop/src/locales/pl/messages.json | 3 +++ apps/desktop/src/locales/pt_BR/messages.json | 3 +++ apps/desktop/src/locales/pt_PT/messages.json | 3 +++ apps/desktop/src/locales/ro/messages.json | 3 +++ apps/desktop/src/locales/ru/messages.json | 3 +++ apps/desktop/src/locales/si/messages.json | 3 +++ apps/desktop/src/locales/sk/messages.json | 3 +++ apps/desktop/src/locales/sl/messages.json | 3 +++ apps/desktop/src/locales/sr/messages.json | 3 +++ apps/desktop/src/locales/sv/messages.json | 3 +++ apps/desktop/src/locales/te/messages.json | 3 +++ apps/desktop/src/locales/th/messages.json | 3 +++ apps/desktop/src/locales/tr/messages.json | 3 +++ apps/desktop/src/locales/uk/messages.json | 9 ++++++--- apps/desktop/src/locales/vi/messages.json | 3 +++ apps/desktop/src/locales/zh_CN/messages.json | 11 +++++++---- apps/desktop/src/locales/zh_TW/messages.json | 3 +++ 63 files changed, 205 insertions(+), 16 deletions(-) diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index c204a8e3d00..c75fdc4db81 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Weens ’n ondernemingsbeleid mag u geen wagwoorde in u persoonlike kluis bewaar nie. Verander die eienaarskap na ’n organisasie en kies uit ’n van die beskikbare versamelings." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "U wagwoordwenk kan nie dieselfde as u wagwoord wees nie." }, diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index dca2856b74d..8223cf9d2b4 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "بسبب سياسة المؤسسة، يمنع عليك حفظ العناصر في خزانتك الشخصية. غيّر خيار الملكية إلى مؤسسة واختر من المجموعات المتاحة." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "لا يمكن أن يكون تلميح كلمة المرور نفس كلمة المرور الخاصة بك." }, diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index eb347605fa4..abfbb6b108a 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Müəssisə siyasətinə görə, elementləri şəxsi seyfinizdə saxlamağınız məhdudlaşdırılıb. Sahiblik seçimini təşkilat olaraq dəyişdirin və mövcud kolleksiyalar arasından seçim edin." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Yeni parolunuz hazırkı parolunuzla eyni ola bilməz." + }, "hintEqualsPassword": { "message": "Parol məsləhəti, parolunuzla eyni ola bilməz." }, diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index 3f3cc67d3cf..287fbb6c4c3 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "У адпаведнасці з палітыкай прадпрыемства вам забаронена захоўваць элементы ў асабістым сховішчы. Змяніце параметры ўласнасці на арганізацыю і выберыце з даступных калекцый." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Падказка для пароля не можа супадаць з паролем." }, diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index c565f67df59..0e47828c86c 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Заради някоя политика за голяма организация не може да запазвате елементи в собствения си трезор. Променете собствеността да е на организация и изберете от наличните колекции." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Новата парола не може да бъде същата като текущата." + }, "hintEqualsPassword": { "message": "Подсказването за паролата не може да съвпада с нея." }, diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index 9343f95a5ef..7d29d4ee663 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "একটি এন্টারপ্রাইজ নীতির কারণে, আপনি আপনার ব্যক্তিগত ভল্টে বস্তুসমূহ সংরক্ষণ করা থেকে সীমাবদ্ধ। একটি প্রতিষ্ঠানের মালিকানা বিকল্পটি পরিবর্তন করুন এবং উপলভ্য সংগ্রহগুলি থেকে চয়ন করুন।" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "আপনার পাসওয়ার্ডের ইঙ্গিতটি আপনার পাসওয়ার্ড হতে পারবে না।" }, diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index a3e915a4bfe..66f8d3b72ac 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Zbog poslovnih smjernica, zabranjeno vam je pohranjivanje predmeta u svoj lični trezor. Promijenite opciju vlasništva u organizaciji i odaberite neku od dostupnih kolekcija." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Podsjetnik za lozinku ne može biti isti kao lozinka." }, diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index a368009b1d2..f3fcc17fed9 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "A causa d'una política empresarial, no podeu guardar elements a la vostra caixa forta personal. Canvieu l'opció Propietat en organització i trieu entre les col·leccions disponibles." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "El vostre suggeriment de contrasenya no pot ser el mateix que la vostra contrasenya." }, @@ -3542,10 +3545,10 @@ "message": "Change account email" }, "allowScreenshots": { - "message": "Allow screen capture" + "message": "Permet capturar la pantalla" }, "allowScreenshotsDesc": { - "message": "Allow the Bitwarden desktop application to be captured in screenshots and viewed in remote desktop sessions. Disabling this will prevent access on some external displays." + "message": "Permet que l'aplicació d'escriptori Bitwarden siga capturada fent captures de pantalla i es visualitze en sessions d'escriptori remot. Si ho desactivem, impedirà l'accés a algunes pantalles externes." }, "confirmWindowStillVisibleTitle": { "message": "Confirm window still visible" diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index 236278b3c5a..980b1f671ef 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Z důvodu podnikových zásad nemůžete ukládat položky do svého osobního trezoru. Změňte vlastnictví položky na organizaci a poté si vyberte z dostupných kolekcí." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Vaše nové heslo nemůže být stejné jako Vaše současné heslo." + }, "hintEqualsPassword": { "message": "Nápověda k Vašemu heslu nemůže být stejná jako Vaše heslo." }, diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index 2657e0cb942..d911e190591 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index 8e5fbd90b68..d36e13dcc83 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Grundet en virksomhedspolitik forhindres du i at gemme emner i din personlige boks. Skift ejerskabsindstillingen til en organisation, og vælg blandt de tilgængelige samlinger." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Adgangskodetip og adgangskoden må ikke være identiske." }, diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index 1aedecedc5c..442023608ee 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Aufgrund einer Unternehmensrichtlinie darfst du keine Einträge in deinem persönlichen Tresor speichern. Ändere die Eigentümer-Option in eine Organisation und wähle aus den verfügbaren Sammlungen." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Dein neues Passwort darf nicht dasselbe sein wie dein aktuelles Passwort." + }, "hintEqualsPassword": { "message": "Dein Passwort-Hinweis darf nicht identisch mit deinem Passwort sein." }, diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index 189f3648984..19ef7d956ba 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Λόγω μιας επιχειρηματικής πολιτικής, περιορίζεστε από την αποθήκευση αντικειμένων στο ατομικό σας θησαυ/κιό. Αλλάξτε την επιλογή ιδιοκτησίας σε έναν οργανισμό και επιλέξτε από τις διαθέσιμες συλλογές." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Η υπόδειξη κωδικού πρόσβασης, δεν μπορεί να είναι η ίδια με τον κωδικό πρόσβασης σας." }, diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index 81eba8202d1..ef5cfa5b46d 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organisation and choose from available collections." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index 6d75ce10763..09003e8f0ab 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Due to an Enterprise Policy, you are restricted from saving items to your personal vault. Change the Ownership option to an organization and choose from available Collections." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index 48b0600724d..8f9cda666f8 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index ff9d805a9af..3ddac01b47c 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Debido a una política de organización, tiene restringido el guardar elementos a su caja fuerte personal. Cambie la configuración de propietario a organización y elija entre las colecciones disponibles." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "La pista para la contraseña no puede ser igual que la contraseña." }, diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index 2f2925cf212..cbec15daf53 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Ettevõtte poliitika tõttu ei saa sa andmeid oma personaalsesse Hoidlasse salvestada. Vali Omanikuks organisatsioon ja vali mõni saadavaolevatest Kogumikest." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Parooli vihje ei saa olla sama mis parool ise." }, diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index e14618f4bc3..6f13b2f3bc4 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Erakundeko politika bat dela eta, ezin dituzu elementuak zure kutxa gotor pertsonalean gorde. Aldatu jabe aukera erakunde aukera batera, eta aukeratu bilduma erabilgarrien artean." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Zure pasahitza ezin da izan zure pasahitzaren pistaren berdina." }, diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index 80836f6a50f..ef67738c03c 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "به دلیل سیاست پرمیوم، برای ذخیره موارد در گاوصندوق شخصی خود محدود شده اید. گزینه مالکیت را به یک سازمان تغییر دهید و مجموعه های موجود را انتخاب کنید." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "اشاره به کلمه عبور شما نمی‌تواند همان کلمه عبور شما باشد." }, diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index 0c2f22dd209..767a1be15ef 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Yrityskäytännön johdosta kohteiden tallennus henkilökohtaiseen holviin ei ole mahdollista. Muuta omistusasetus organisaatiolle ja valitse käytettävissä olevista kokoelmista." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Salasanavihjeesi ei voi olla sama kuin salasanasi." }, diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index 0e7b15a7f62..597dd84d781 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Dahil sa isang patakaran sa enterprise, pinaghihigpitan ka mula sa pag-save ng mga item sa iyong vault. Baguhin ang pagpipilian sa pagmamay ari sa isang organisasyon at pumili mula sa mga magagamit na koleksyon." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Ang iyong password hint ay hindi maaaring pareho sa iyong password." }, diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index 4143cc34d22..94edd101367 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "En raison d'une politique d'entreprise, il vous est interdit d'enregistrer des éléments dans votre coffre personnel. Sélectionnez une organisation dans l'option Propriété et choisissez parmi les collections disponibles." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Votre indice de mot de passe ne peut pas être identique à votre nom d'utilisateur." }, diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index f1c63780793..8850cbe5a3f 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index 42d5e8a74cc..46288ba0b63 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "בשל מדיניות ארגונית, אתה מוגבל מלשמור פריטים לכספת האישית שלך. שנה את אפשרות הבעלות לארגון ובחר מאוספים זמינים." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "רמז הסיסמה שלך לא יכול להיות אותו הדבר כמו הסיסמה שלך." }, diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index 4ac5136a684..27a9eb7e92f 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index 5b1098328e4..240f7406db1 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Pravila tvrtke onemogućuju spremanje stavki u osobni trezor. Promijeni vlasništvo stavke na tvrtku i odaberi dostupnu Zbirku." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Podsjetnik za lozinku ne može biti isti kao lozinka." }, diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index 894dac4402f..e2caf838a66 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Egy vállalati házirend miatt korlátozásra került az elemek személyes tárolóba történő mentése. Módosítsuk a Tulajdon opciót egy szervezetre és válasszunk az elérhető gyűjtemények közül." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Az új jelszó nem lehet azonos a jelenlegi jelszóval." + }, "hintEqualsPassword": { "message": "A jelszavas tipp nem lehet azonos a jelszóval." }, diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index 1ea5c94e861..4a95781d04e 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Karena Kebijakan Perusahaan, Anda dilarang menyimpan item ke lemari besi pribadi Anda. Ubah opsi Kepemilikan ke organisasi dan pilih dari Koleksi yang tersedia." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Petunjuk kata sandi Anda tidak boleh sama dengan kata sandi Anda." }, diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index 2bb11cee1d2..5376e974ae5 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "A causa di una politica aziendale, non puoi salvare elementi nella tua cassaforte personale. Cambia l'opzione di proprietà in un'organizzazione e scegli tra le raccolte disponibili." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Il suggerimento per la password non può essere uguale alla tua password." }, diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index eaa80931539..5f9e7cbc6ab 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -429,10 +429,10 @@ "message": "名前は必須項目です。" }, "addedItem": { - "message": "追加されたアイテム" + "message": "追加しました" }, "editedItem": { - "message": "編集されたアイテム" + "message": "保存しました" }, "deleteItem": { "message": "アイテムの削除" @@ -447,7 +447,7 @@ "message": "このアイテムを削除してもよろしいですか?" }, "deletedItem": { - "message": "削除しました" + "message": "ごみ箱に移動しました" }, "overwritePasswordConfirmation": { "message": "現在のパスワードを上書きしてもよろしいですか?" @@ -595,7 +595,7 @@ "message": "添付ファイルの追加" }, "deletedAttachment": { - "message": "削除された添付ファイル" + "message": "添付ファイルを削除しました" }, "deleteAttachmentConfirmation": { "message": "この添付ファイルを削除してよろしいですか?" @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "組織のポリシーにより、個人保管庫へのアイテムの保存が制限されています。 所有権を組織に変更し、利用可能なコレクションから選択してください。" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "新しいパスワードを現在のパスワードと同じにすることはできません。" + }, "hintEqualsPassword": { "message": "パスワードのヒントをパスワードと同じにすることはできません。" }, @@ -3563,6 +3566,6 @@ "message": "危険なパスワードの変更" }, "move": { - "message": "Move" + "message": "移動" } } diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index 2e7214e5170..6e3101404e0 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index f1c63780793..8850cbe5a3f 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index 2869cffeed2..6dc91ad0ca8 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Due to an Enterprise Policy, you are restricted from saving items to your personal vault. Change the Ownership option to an organization and choose from available Collections." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "ನಿಮ್ಮ ಪಾಸ್‌ವರ್ಡ್ ಸುಳಿವು ನಿಮ್ಮ ಪಾಸ್‌ವರ್ಡ್‌ನಂತೆಯೇ ಇರಬಾರದು." }, diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index ed69ad34fdf..d684a8ed405 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "엔터프라이즈 정책으로 인해 개인 보관함에 항목을 저장할 수 없습니다. 조직에서 소유권 설정을 변경한 다음, 사용 가능한 컬렉션 중에서 선택해주세요." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "비밀번호 힌트는 비밀번호와 같을 수 없습니다." }, diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index 1d20b814539..0a4df541985 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Dėl įmonės politikos jums neleidžiama saugoti daiktų asmeninėje saugykloje. Pakeiskite nuosavybės parinktį į organizaciją ir pasirinkite iš galimų rinkinių." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Jūsų slaptažodžio užuomina negali būti lygi jūsų slaptažodžiui." }, diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index 7a4c7343272..8ab3949bc64 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -1852,7 +1852,7 @@ "message": "Glabātavas noildzes darbība" }, "vaultTimeoutActionLockDesc": { - "message": "Ir nepieciešams atkārtoti ievadīt galveno paroli, lai piekļūt aizslēgtai glabātavai." + "message": "Ir nepieciešama galvenā parole vai cits atslēgšanas veids, lai atkal piekļūtu savai glabātavai." }, "vaultTimeoutActionLogOutDesc": { "message": "Ir nepieciešama atkārtota pieteikšanās, lai atkal piekļūtu glabātavai." @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Uzņēmuma nosacījumi liedz saglabāt vienumus privātajā glabātavā. Ir jānorāda piederība apvienībai un jāizvēlas kāds no pieejamajiem krājumiem." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Jaunā parole nevar būt tāda pati kā pašreizējā." + }, "hintEqualsPassword": { "message": "Paroles norāde nedrīkst būt tāda pati kā parole." }, @@ -2491,7 +2494,7 @@ "message": "Aizslēgta" }, "yourVaultIsLockedV2": { - "message": "Glabātava ir slēgta" + "message": "Glabātava ir aizslēgta" }, "unlocked": { "message": "Atslēgta" diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index c3c761f1ffb..5ce7ecd403c 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index 3b22d0369ab..568095899f3 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index f1c63780793..8850cbe5a3f 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index d79065b8cde..281d947d98f 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index 2873e2a931a..0318b040250 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "På grunn av bedrifsretningslinjer er du begrenset fra å lagre objekter til ditt personlige hvelv. Endre alternativ for eierskap til en organisasjon og velg blant tilgjengelige samlinger." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Passordhintet ditt kan ikke være det samme som passordet ditt." }, diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index 5fdbfe2bf02..3fffd5caf74 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index 782decad1ef..6e7d8eab7ed 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Wegens bedrijfsbeleid mag je geen wachtwoorden opslaan in je persoonlijke kluis. Verander het eigenaarschap naar een organisatie en kies uit een van de beschikbare collecties." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Je nieuwe wachtwoord moet anders zijn dan je huidige wachtwoord." + }, "hintEqualsPassword": { "message": "Je wachtwoordhint moet anders zijn dan je wachtwoord." }, diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index 2b8acdf0a93..6e0c28ca5ae 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index 075ad16dd2d..790a5a7b15b 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index 78d3da851fa..1b36128cc68 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Ze względu na zasadę przedsiębiorstwa, nie możesz zapisywać elementów w osobistym sejfie. Zmień właściciela elementu na organizację i wybierz jedną z dostępnych kolekcji." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Podpowiedź do hasła nie może być taka sama jak hasło." }, diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index d5cedf8fc33..5dcb648f5a3 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Devido a uma Política Empresarial, você está restrito de salvar itens para seu cofre pessoal. Altere a opção de Propriedade para uma organização e escolha entre as Coleções disponíveis." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Sua nova senha não pode ser a mesma que a sua atual." + }, "hintEqualsPassword": { "message": "Sua dica de senha não pode ser o mesmo que sua senha." }, diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index 265df8b7566..2a1f1613b92 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Devido a uma política empresarial, está impedido de guardar itens no seu cofre pessoal. Altere a opção Propriedade para uma organização e escolha entre as coleções disponíveis." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "A sua nova palavra-passe não pode ser igual à sua palavra-passe atual." + }, "hintEqualsPassword": { "message": "A dica da sua palavra-passe não pode ser igual à sua palavra-passe." }, diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index 002a0a9398b..5ff57f0e3ec 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Din cauza unei politici a întreprinderii, nu vă puteți salva elemente în seiful individual. Schimbați opțiunea de proprietate la o organizație și alegeți din colecțiile disponibile." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Indiciul dvs. de parolă nu poate fi același cu parola dvs." }, diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index c86747af746..193a6352254 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "В соответствии с корпоративной политикой вам запрещено сохранять элементы в личном хранилище. Измените владельца на организацию и выберите из доступных Коллекций." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Ваш новый пароль не может быть таким же, как текущий." + }, "hintEqualsPassword": { "message": "Подсказка для пароля не может совпадать с паролем." }, diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index ecc5a8f29bc..6a2e09c0292 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index e059566ddd7..35c53a31296 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Z dôvodu podnikovej politiky máte obmedzené ukladanie položiek do osobného trezora. Zmeňte možnosť vlastníctvo na organizáciu a vyberte si z dostupných zbierok." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Nové heslo nemôže byť rovnaké ako súčasné heslo." + }, "hintEqualsPassword": { "message": "Nápoveď k heslu nemôže byť rovnaká ako heslo." }, diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index 0a911f69546..6fff29c7b4b 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index 806c2115a04..20f63b32703 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Због смерница за предузећа, ограничено вам је чување предмета у вашем личном трезору. Промените опцију власништва у организацију и изаберите из доступних колекција." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Ваш савет за лозинку не може да буде исти као лозинка." }, diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index f257b51e1c5..2f051323f9f 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "På grund av en av företagets policyer är du begränsad från att spara objekt till ditt personliga valv. Ändra ägarskap till en organisation och välj från tillgängliga samlingar." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Din lösenordsledtråd får inte vara samma som ditt lösenord." }, diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index f1c63780793..8850cbe5a3f 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index c0e8a91476a..0edc3fe527a 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index 53c556b0383..76dc32239be 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Bir kuruluş ilkesi nedeniyle kişisel kasanıza hesap kaydetmeniz kısıtlanmış. Sahip seçeneğini bir kuruluş olarak değiştirin ve mevcut koleksiyonlar arasından seçim yapın." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Parola ipucunuz parolanızla aynı olamaz." }, diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index 07fccfc7d41..f699dda8311 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -2072,8 +2072,11 @@ "personalOwnershipSubmitError": { "message": "Згідно з політикою компанії, вам заборонено зберігати записи в особистому сховищі. Змініть опцію власника на організацію та виберіть серед доступних збірок." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Новий пароль повинен відрізнятися від поточного." + }, "hintEqualsPassword": { - "message": "Підказка для пароля не може бути такою самою, як ваш пароль." + "message": "Підказка повинна відрізнятися від пароля." }, "personalOwnershipPolicyInEffect": { "message": "Політика організації впливає на ваші параметри власності." @@ -2467,7 +2470,7 @@ } }, "exportingIndividualVaultWithAttachmentsDescription": { - "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "message": "Будуть експортовані лише записи особистого сховища, включно з вкладеннями, пов'язані з $EMAIL$. Записи сховища організації не експортуватимуться.", "placeholders": { "email": { "content": "$1", @@ -3563,6 +3566,6 @@ "message": "Змінити ризикований пароль" }, "move": { - "message": "Move" + "message": "Перемістити" } } diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index b0c6b08d6c7..fa426f3de2f 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "Do chính sách doanh nghiệp, bạn bị hạn chế lưu các mục vào kho cá nhân của mình. Thay đổi tùy chọn quyền sở hữu thành một tổ chức và chọn từ các bộ sưu tập có sẵn." }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Gợi ý mật khẩu không được trùng với mật khẩu của bạn." }, diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index cb6e63e398c..3dd411f6593 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -1623,19 +1623,19 @@ "message": "此密码将用于导出和导入此文件" }, "accountRestrictedOptionDescription": { - "message": "使用衍生自您账户的用户名和主密码的账户加密密钥,以加密此导出并限制只能导入到当前的 Bitwarden 账户。" + "message": "使用衍生自您账户用户名和主密码的加密密钥以加密此导出,并限制只能导入到当前 Bitwarden 账户。" }, "passwordProtected": { "message": "密码保护" }, "passwordProtectedOptionDescription": { - "message": "设置一个文件密码用来加密此导出,并使用此密码解密以导入到任意 Bitwarden 账户。" + "message": "设置一个文件密码以加密此导出,并使用此密码解密以导入到任意 Bitwarden 账户。" }, "exportTypeHeading": { "message": "导出类型" }, "accountRestricted": { - "message": "账户受限" + "message": "账户限制" }, "filePasswordAndConfirmFilePasswordDoNotMatch": { "message": "「文件密码」与「确认文件密码」不一致。" @@ -1980,7 +1980,7 @@ "message": "随时。" }, "byContinuingYouAgreeToThe": { - "message": "若继续,代表您同意" + "message": "若继续,表示您同意" }, "and": { "message": "和" @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "由于某个企业策略,您不能将项目保存到您的个人密码库。将所有权选项更改为组织,并从可用的集合中选择。" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "您的新密码不能与当前密码相同。" + }, "hintEqualsPassword": { "message": "您的密码提示不能与您的密码相同。" }, diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index 43d3dc4d9a4..8e3caf38218 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -2072,6 +2072,9 @@ "personalOwnershipSubmitError": { "message": "由於生效中的某個企業原則,您不可將項目儲存至您的個人密碼庫。請將所有權變更為組織帳號,並選擇可用的分類。" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "密碼提示不能與您的密碼相同。" }, From 732029b3f2c8658eae866adc383c56ba77436766 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Fri, 11 Apr 2025 10:16:15 +0200 Subject: [PATCH 05/19] Autosync the updated translations (#14233) 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 | 21 ++++++----- apps/browser/src/_locales/be/messages.json | 3 ++ apps/browser/src/_locales/bg/messages.json | 3 ++ apps/browser/src/_locales/bn/messages.json | 3 ++ apps/browser/src/_locales/bs/messages.json | 3 ++ apps/browser/src/_locales/ca/messages.json | 3 ++ 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 | 5 ++- 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 | 5 ++- apps/browser/src/_locales/hi/messages.json | 3 ++ apps/browser/src/_locales/hr/messages.json | 3 ++ apps/browser/src/_locales/hu/messages.json | 3 ++ apps/browser/src/_locales/id/messages.json | 3 ++ apps/browser/src/_locales/it/messages.json | 3 ++ apps/browser/src/_locales/ja/messages.json | 31 ++++++++-------- 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 | 3 ++ apps/browser/src/_locales/lt/messages.json | 3 ++ apps/browser/src/_locales/lv/messages.json | 25 +++++++------ 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 | 21 ++++++----- 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 | 5 ++- 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/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 | 35 ++++++++++--------- apps/browser/src/_locales/vi/messages.json | 3 ++ apps/browser/src/_locales/zh_CN/messages.json | 13 ++++--- apps/browser/src/_locales/zh_TW/messages.json | 3 ++ 60 files changed, 247 insertions(+), 67 deletions(-) diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 0a8884951bb..3ca64543a4f 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "سياسة الخصوصية" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "لا يمكن أن يكون تلميح كلمة المرور نفس كلمة المرور الخاصة بك." }, diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 358a7596f56..07a86b42565 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Gizlilik Siyasəti" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Yeni parolunuz hazırkı parolunuzla eyni ola bilməz." + }, "hintEqualsPassword": { "message": "Parol ipucusu, parolunuzla eyni ola bilməz." }, @@ -5142,30 +5145,30 @@ "message": "Riskli parolları dəyişdir" }, "introCarouselLabel": { - "message": "Welcome to Bitwarden" + "message": "Bitwarden-ə xoş gəlmisiniz" }, "securityPrioritized": { - "message": "Security, prioritized" + "message": "Təhlükəsizlik, prioritetdir" }, "securityPrioritizedBody": { - "message": "Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you." + "message": "Girişləri, kart və kimlik məlumatlarınızı güvənli seyfinizdə saxlayın. Bitwarden, sizin üçün mühüm olanları qorumaq üçün zero-knowledge və ucdan-uca şifrələmə istifadə edir." }, "quickLogin": { - "message": "Quick and easy login" + "message": "Cəld və asan giriş" }, "quickLoginBody": { - "message": "Set up biometric unlock and autofill to log into your accounts without typing a single letter." + "message": "Tək bir hərf yazmadan hesablarınıza giriş etmək üçün biometrik kilid açma və avto-doldurma seçimlərini qurun." }, "secureUser": { - "message": "Level up your logins" + "message": "Giriş məlumatlarınızı yüksəldin" }, "secureUserBody": { - "message": "Use the generator to create and save strong, unique passwords for all your accounts." + "message": "Bütün hesablarınız üçün güclü, unikal parollar yaratmaq və saxlamaq üçün yaradıcını istifadə edin." }, "secureDevices": { - "message": "Your data, when and where you need it" + "message": "Datanız, ehtiyacınız olan vaxt yanınızdadır" }, "secureDevicesBody": { - "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + "message": "Bitwarden mobil, brauzer və masaüstü tətbiqləri ilə limitsiz cihaz arasında limitsiz parol saxlayın." } } diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index ab2889ec99a..7240371ca2d 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Палітыка прыватнасці" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Падказка для пароля не можа супадаць з паролем." }, diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 347fba7e93f..88128e68be1 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Политика за поверителност" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Новата парола не може да бъде същата като текущата." + }, "hintEqualsPassword": { "message": "Подсказването за паролата не може да съвпада с нея." }, diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index 0806f936677..7a7a9cfa981 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "গোপনীয়তা নীতি" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "আপনার পাসওয়ার্ডের ইঙ্গিতটি আপনার পাসওয়ার্ড হতে পারবে না।" }, diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 3d17255e801..0e73dc0b92e 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Privacy Policy" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index 59631bc1670..3555642ef90 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Declaració de privadesa" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "El vostre suggeriment de contrasenya no pot ser el mateix que la vostra contrasenya." }, diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index be338aee089..361d5314e5d 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Zásady ochrany osobních údajů" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Vaše nové heslo nemůže být stejné jako Vaše současné heslo." + }, "hintEqualsPassword": { "message": "Nápověda k Vašemu heslu nemůže být stejná jako Vaše heslo." }, diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index c6a59456c46..c0034df9d2b 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Polisi preifatrwydd" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index 55d6cad20e4..c6f051ce811 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Fortrolighedspolitik" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Dit adgangskodetip kan ikke være det samme som din adgangskode." }, diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index fa77ca964ad..274bc934b39 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Datenschutzbestimmungen" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Dein neues Passwort darf nicht dasselbe sein wie dein aktuelles Passwort." + }, "hintEqualsPassword": { "message": "Dein Passwort-Hinweis darf nicht identisch mit deinem Passwort sein." }, @@ -5166,6 +5169,6 @@ "message": "Deine Daten, wann und wo du sie brauchst" }, "secureDevicesBody": { - "message": "Speicher eine unbegrenzte Anzahl von Passwörter auf unbegrenzt vielen Geräten mit Bitwarden-Apps für Smartphones, Browser und Desktop." + "message": "Speicher eine unbegrenzte Anzahl von Passwörtern auf unbegrenzt vielen Geräten mit Bitwarden-Apps für Smartphones, Browser und Desktop." } } diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index f1e6c6cb6e1..afd76596c85 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Πολιτική Απορρήτου" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Η υπόδειξη κωδικού πρόσβασης, δεν μπορεί να είναι η ίδια με τον κωδικό πρόσβασης σας." }, diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index 321e17eaa75..3b968f11587 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Privacy policy" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 2432a120295..f32abc028ed 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Privacy Policy" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index 55379670548..e031a4487d6 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Política de privacidad" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Tu contraseña no puede ser idéntica a la pista de contraseña." }, diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 9a1b4037b8f..670dab1fcda 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Privaatsuspoliitika" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Parooli vihje ei saa olla sama mis parool ise." }, diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 2d04203bc01..b5ffbf45ba2 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Pribatutasun politika" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Zure pasahitza ezin da izan zure pasahitzaren pistaren berdina." }, diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index e7d5f58e49c..d8a8f827db4 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "سیاست حفظ حریم خصوصی" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "اشاره به کلمه عبور شما نمی‌تواند همان کلمه عبور شما باشد." }, diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index e108f2ceab6..56e25aead7d 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Tietosuojakäytäntö" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Salasanavihjeesi ei voi olla sama kuin salasanasi." }, diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index e288bd2195a..69d5895ba27 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Patakaran sa Privacy" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Hindi pwedeng maging pareho ang password hint at password mo." }, diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 254c18556f7..fe2127148c8 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Politique de confidentialité" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Votre indice de mot de passe ne peut pas être identique à votre mot de passe." }, diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index efed6c4bbd4..83af8b6db75 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Política de Privacidade" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "A pista do contrasinal non pode ser o contrasinal." }, diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index 7350c865e5e..605dd5454f0 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -950,7 +950,7 @@ "message": "הסיסמה הועתקה" }, "uri": { - "message": "כתובת" + "message": "URI" }, "uriPosition": { "message": "כתובת $POSITION$", @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "מדיניות הפרטיות" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "לא ניתן להשתמש בסיסמה בתור רמז לסיסמה." + }, "hintEqualsPassword": { "message": "רמז הסיסמה שלך לא יכול להיות זהה לסיסמה שלך." }, diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 1815ab4118b..9cc0f5251e6 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "प्राइवेसी पोलिसी" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "आपका पासवर्ड संकेत आपके पासवर्ड के समान नहीं हो सकता है।" }, diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index 5c6cf17f02c..bef7c9b8f13 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Pravila privatnosti" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Podsjetnik za lozinku ne može biti isti kao lozinka." }, diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index 50f02f2bc75..d0114b30b7b 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Adatvédelem" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Az új jelszó nem lehet azonos a jelenlegi jelszóval." + }, "hintEqualsPassword": { "message": "A jelszavas tipp nem lehet azonos a jelszóval." }, diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 91a4447b9b2..16b11c9e732 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Kebijakan Privasi" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Petunjuk kata sandi Anda tidak boleh sama dengan kata sandi Anda." }, diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index c3ecb237732..1448776e430 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Informativa sulla Privacy" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Il suggerimento della password non può essere uguale alla password." }, diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 9bacc66bce3..4aae076174e 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "プライバシーポリシー" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "新しいパスワードを現在のパスワードと同じにすることはできません。" + }, "hintEqualsPassword": { "message": "パスワードのヒントをパスワードと同じにすることはできません。" }, @@ -2505,14 +2508,14 @@ "description": "Description of the review at-risk login slide on the at-risk password page carousel" }, "reviewAtRiskLoginSlideImgAltPeriod": { - "message": "Illustration of a list of logins that are at-risk." + "message": "危険な状態にあるログイン情報の一覧表示の例" }, "generatePasswordSlideDesc": { "message": "Bitwarden の自動入力メニューで、強力で一意なパスワードをすぐに生成しましょう。", "description": "Description of the generate password slide on the at-risk password page carousel" }, "generatePasswordSlideImgAltPeriod": { - "message": "Illustration of the Bitwarden autofill menu displaying a generated password." + "message": "Bitwarden の自動入力メニューで、生成されたパスワードが表示されている例" }, "updateInBitwarden": { "message": "Bitwarden 上のデータを更新" @@ -2522,7 +2525,7 @@ "description": "Description of the update in Bitwarden slide on the at-risk password page carousel" }, "updateInBitwardenSlideImgAltPeriod": { - "message": "Illustration of a Bitwarden’s notification prompting the user to update the login." + "message": "ユーザーにログイン情報を更新するよう促す Bitwarden の通知例" }, "turnOnAutofill": { "message": "自動入力をオンにする" @@ -4244,7 +4247,7 @@ } }, "viewItemTitleWithField": { - "message": "View item - $ITEMNAME$ - $FIELD$", + "message": "アイテムを表示 - $ITEMNAME$ - $FIELD$", "description": "Title for a link that opens a view for an item.", "placeholders": { "itemname": { @@ -4268,7 +4271,7 @@ } }, "autofillTitleWithField": { - "message": "Autofill - $ITEMNAME$ - $FIELD$", + "message": "自動入力 - $ITEMNAME$ - $FIELD$", "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { @@ -5142,30 +5145,30 @@ "message": "危険なパスワードの変更" }, "introCarouselLabel": { - "message": "Welcome to Bitwarden" + "message": "Bitwarden へようこそ" }, "securityPrioritized": { - "message": "Security, prioritized" + "message": "セキュリティが優先" }, "securityPrioritizedBody": { - "message": "Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you." + "message": "ログイン情報、カード、ID を安全な保管庫に保存します。 Bitwarden はゼロ知識、エンドツーエンドの暗号化を使用して、あなたにとって重要なものを保護します。" }, "quickLogin": { - "message": "Quick and easy login" + "message": "すばやく簡単にログイン" }, "quickLoginBody": { - "message": "Set up biometric unlock and autofill to log into your accounts without typing a single letter." + "message": "生体認証によるロック解除と自動入力を設定すると、文字を一切入力することなく各種アカウントにログインすることができます。" }, "secureUser": { - "message": "Level up your logins" + "message": "ログインをレベルアップ" }, "secureUserBody": { - "message": "Use the generator to create and save strong, unique passwords for all your accounts." + "message": "パスワード生成機能を使用すると、すべてのアカウントで強力な一意のパスワードを作成して保存できます。" }, "secureDevices": { - "message": "Your data, when and where you need it" + "message": "必要なデータを、いつでもどこでも" }, "secureDevicesBody": { - "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + "message": "Bitwarden のモバイル、ブラウザ、デスクトップアプリでは、保存できるパスワード数やデバイス数に制限はありません。" } } diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index 8ad1efeb4a2..132a9b4d4ed 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "კონფიდენციალობის პოლიტიკა" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index c43d83029c9..a9f5f5c2d75 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Privacy Policy" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 8415a9fbf3f..9e137fd6156 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "ಗೌಪ್ಯತಾ ನೀತಿ" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "ನಿಮ್ಮ ಪಾಸ್‌ವರ್ಡ್ ಸುಳಿವು ನಿಮ್ಮ ಪಾಸ್‌ವರ್ಡ್‌ನಂತೆಯೇ ಇರಬಾರದು." }, diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index 00d3c7e31ac..dfc7364710a 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "개인 정보 보호 정책" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "비밀번호 힌트는 비밀번호와 같을 수 없습니다." }, diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 53dc8a85743..93bbbef7292 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Privatumo politika" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Jūsų slaptažodžio užuomina negali būti tokia pati kaip Jūsų slaptažodis." }, diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index d9438c91bcb..a42ee2ace78 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -665,7 +665,7 @@ "message": "Glabātava ir aizslēgta. Jāapliecina sava identitāte, lai turpinātu." }, "yourVaultIsLockedV2": { - "message": "Glabātava ir slēgta" + "message": "Glabātava ir aizslēgta" }, "yourAccountIsLocked": { "message": "Konts ir slēgts" @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Privātuma nosacījumi" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Jaunā parole nevar būt tāda pati kā pašreizējā." + }, "hintEqualsPassword": { "message": "Paroles norāde nedrīkst būt tāda pati kā parole." }, @@ -5139,33 +5142,33 @@ "message": "Lai izmantotu atslēgšanu ar biometriju, lūgums atjaunināt darbvirsmas lietotni vai atspējot atslēgšanu ar pirkstu nospiedumu darbvirsmas iestatījumos." }, "changeAtRiskPassword": { - "message": "Change at-risk password" + "message": "Mainīt riskam pakļautu paroli" }, "introCarouselLabel": { - "message": "Welcome to Bitwarden" + "message": "Laipni lūdzam Bitwarden" }, "securityPrioritized": { - "message": "Security, prioritized" + "message": "Drošība pirmajā vietā" }, "securityPrioritizedBody": { - "message": "Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you." + "message": "Pieteikšanās vienumu, karšu un identitāšu glabāšana savā drošajā glabātavā. Bitwarden izmanto nulles zināšanu pilnīgu šifrēšanu, lai aizsargātu to, kas Tev ir svarīgs." }, "quickLogin": { - "message": "Quick and easy login" + "message": "Ātra un vienkārša pieteikšanās" }, "quickLoginBody": { - "message": "Set up biometric unlock and autofill to log into your accounts without typing a single letter." + "message": "Iestati biometrisko atslēgšanu un automātiski aizpildi, lai pieteiktos savos kontos bez neviena burta ievadīšanas." }, "secureUser": { - "message": "Level up your logins" + "message": "Uzlabo savu pieteikšanos" }, "secureUserBody": { - "message": "Use the generator to create and save strong, unique passwords for all your accounts." + "message": "Veidotājs ir izmantojams, lai izveidotu un saglabātu spēcīgas, neatkārtojamas paroles visiem Taviem kontiem." }, "secureDevices": { - "message": "Your data, when and where you need it" + "message": "Tavi dati, kad un kur vien tie ir nepieciešami" }, "secureDevicesBody": { - "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + "message": "Neierobežotu paroļu skaitu var saglabāt neierobežotā ierīdžu daudzumā ar Bitwarden viedtālruņa, pārlūka un darbvirsmas lietotni." } } diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index a1447331124..5810d5392e6 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "സ്വകാര്യതാനയം" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index 8832e30fa0c..303a2cf96dd 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Privacy Policy" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index c43d83029c9..a9f5f5c2d75 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Privacy Policy" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 303f9fc8487..18d00d9c274 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Personvernerklæring" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Passordhintet ditt kan ikke være det samme som passordet ditt." }, diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index c43d83029c9..a9f5f5c2d75 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Privacy Policy" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index 2f1ecad557f..80eba21ddb3 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Privacybeleid" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Je nieuwe wachtwoord moet anders zijn dan je huidige wachtwoord." + }, "hintEqualsPassword": { "message": "Je wachtwoordhint moet anders zijn dan je wachtwoord." }, diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index c43d83029c9..a9f5f5c2d75 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Privacy Policy" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index c43d83029c9..a9f5f5c2d75 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Privacy Policy" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 13587a74e52..5e74025800e 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Polityka prywatności" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Twoje nowe hasło nie może być takie samo jak Twoje aktualne hasło." + }, "hintEqualsPassword": { "message": "Podpowiedź do hasła nie może być taka sama jak hasło." }, diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 3ffceca3611..f22958d0498 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Política de Privacidade" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Sua nova senha não pode ser a mesma que a sua atual." + }, "hintEqualsPassword": { "message": "Sua dica de senha não pode ser o mesmo que sua senha." }, @@ -5142,30 +5145,30 @@ "message": "Alterar senhas vulneráveis" }, "introCarouselLabel": { - "message": "Welcome to Bitwarden" + "message": "Bem-vindo(a) ao Bitwarden" }, "securityPrioritized": { - "message": "Security, prioritized" + "message": "Segurança em primeiro lugar" }, "securityPrioritizedBody": { - "message": "Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you." + "message": "Guarde suas senhas, cartões e documentos no seu cofre seguro. Bitwarden usa criptografia ponta-a-ponta de zero-knowledge, protegendo o que é valioso para você." }, "quickLogin": { - "message": "Quick and easy login" + "message": "Login rápido e fácil" }, "quickLoginBody": { - "message": "Set up biometric unlock and autofill to log into your accounts without typing a single letter." + "message": "Ative o desbloqueio por biometria e o autopreenchimento para acessar suas contas sem digitar uma única letra." }, "secureUser": { - "message": "Level up your logins" + "message": "Melhore seus logins de nível" }, "secureUserBody": { - "message": "Use the generator to create and save strong, unique passwords for all your accounts." + "message": "Use o gerador de senhas para criar e salvar senhas fortes e únicas para todas as suas contas." }, "secureDevices": { - "message": "Your data, when and where you need it" + "message": "Seus dados, quando e onde precisar" }, "secureDevicesBody": { - "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + "message": "Guarde quantas senhas quiser e acesse de qualquer lugar com o Bitwarden. No seu celular, navegador e computador." } } diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index a4e0a0de541..172e748a3f1 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Política de privacidade" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "A sua nova palavra-passe não pode ser igual à sua palavra-passe atual." + }, "hintEqualsPassword": { "message": "A dica da sua palavra-passe não pode ser igual à sua palavra-passe." }, diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 9ce305ff74c..0272add140a 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Politică de confidențialitate" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Indiciul dvs. de parolă nu poate fi același cu parola dvs." }, diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index 3847f3eb4fc..639a3b740db 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Политика конфиденциальности" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Ваш новый пароль не может быть таким же, как текущий." + }, "hintEqualsPassword": { "message": "Подсказка для пароля не может совпадать с паролем." }, diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index db54d7f7c18..70c1ff7126a 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "රහස්යතා ප්රතිපත්තිය" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "ඔබගේ මුරපද ඉඟිය ඔබගේ මුරපදයට සමාන විය නොහැක." }, diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index 555e01fec20..0b0a41a49b2 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -973,7 +973,7 @@ "message": "Pridaná položka" }, "editedItem": { - "message": "Upravená položka" + "message": "Položka upravená" }, "deleteItemConfirmation": { "message": "Naozaj chcete odstrániť túto položku?" @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Zásady ochrany osobných údajov" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Nové heslo nemôže byť rovnaké ako súčasné heslo." + }, "hintEqualsPassword": { "message": "Nápoveď k heslu nemôže byť rovnaká ako heslo." }, diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index c7dc9590fe0..0fc3b26acb8 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Pravilnik o zasebnosti" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Namig za geslo ne sme biti enak geslu." }, diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index 2fdbad437c9..c402372895e 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Политика приватности" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Ваш савет за лозинку не може да буде исти као лозинка." }, diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 15de82320b8..74b9bbccc80 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Integritetspolicy" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Din lösenordsledtråd får inte vara samma som ditt lösenord." }, diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index c43d83029c9..a9f5f5c2d75 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Privacy Policy" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 8ac6e7197d8..0ef98835f18 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Privacy Policy" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index a19d4977ed5..f009a52a951 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Gizlilik Politikası" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Parola ipucunuz parolanızla aynı olamaz." }, diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index 6558796bfb5..11e5b304611 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -2297,8 +2297,11 @@ "privacyPolicy": { "message": "Політика приватності" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Новий пароль повинен відрізнятися від поточного." + }, "hintEqualsPassword": { - "message": "Підказка для пароля не може бути такою самою, як ваш пароль." + "message": "Підказка повинна відрізнятися від пароля." }, "ok": { "message": "Ok" @@ -2505,14 +2508,14 @@ "description": "Description of the review at-risk login slide on the at-risk password page carousel" }, "reviewAtRiskLoginSlideImgAltPeriod": { - "message": "Illustration of a list of logins that are at-risk." + "message": "Ілюстрація списку ризикованих записів." }, "generatePasswordSlideDesc": { "message": "Швидко згенеруйте надійний, унікальний пароль через меню автозаповнення Bitwarden на сайті з ризикованим паролем.", "description": "Description of the generate password slide on the at-risk password page carousel" }, "generatePasswordSlideImgAltPeriod": { - "message": "Illustration of the Bitwarden autofill menu displaying a generated password." + "message": "Ілюстрація меню автозаповнення Bitwarden, що показує згенерований пароль." }, "updateInBitwarden": { "message": "Оновити в Bitwarden" @@ -2522,7 +2525,7 @@ "description": "Description of the update in Bitwarden slide on the at-risk password page carousel" }, "updateInBitwardenSlideImgAltPeriod": { - "message": "Illustration of a Bitwarden’s notification prompting the user to update the login." + "message": "Ілюстрація сповіщення Bitwarden, що спонукає користувача оновити пароль." }, "turnOnAutofill": { "message": "Увімкніть автозаповнення" @@ -2978,7 +2981,7 @@ } }, "exportingIndividualVaultWithAttachmentsDescription": { - "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", + "message": "Будуть експортовані лише записи особистого сховища, включно з вкладеннями, пов'язані з $EMAIL$. Записи сховища організації не експортуватимуться.", "placeholders": { "email": { "content": "$1", @@ -4244,7 +4247,7 @@ } }, "viewItemTitleWithField": { - "message": "View item - $ITEMNAME$ - $FIELD$", + "message": "Перегляд запису – $ITEMNAME$ – $FIELD$", "description": "Title for a link that opens a view for an item.", "placeholders": { "itemname": { @@ -4268,7 +4271,7 @@ } }, "autofillTitleWithField": { - "message": "Autofill - $ITEMNAME$ - $FIELD$", + "message": "Автозаповнення – $ITEMNAME$ – $FIELD$", "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { @@ -5142,30 +5145,30 @@ "message": "Змінити ризикований пароль" }, "introCarouselLabel": { - "message": "Welcome to Bitwarden" + "message": "Вітаємо в Bitwarden" }, "securityPrioritized": { - "message": "Security, prioritized" + "message": "Безпека понад усе" }, "securityPrioritizedBody": { - "message": "Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you." + "message": "Зберігайте записи входу, картки та посвідчення в захищеному сховищі. Для захисту ваших даних Bitwarden використовує наскрізне шифрування з нульовим рівнем доступу." }, "quickLogin": { - "message": "Quick and easy login" + "message": "Швидкий і легкий вхід" }, "quickLoginBody": { - "message": "Set up biometric unlock and autofill to log into your accounts without typing a single letter." + "message": "Налаштуйте біометричне розблокування та автозаповнення, щоб миттєво входити в облікові записи." }, "secureUser": { - "message": "Level up your logins" + "message": "Вдоскональте свої паролі" }, "secureUserBody": { - "message": "Use the generator to create and save strong, unique passwords for all your accounts." + "message": "Використовуйте генератор, щоб створювати й зберігати надійні, унікальні паролі для всіх облікових записів." }, "secureDevices": { - "message": "Your data, when and where you need it" + "message": "Ваші дані завжди з вами" }, "secureDevicesBody": { - "message": "Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps." + "message": "Зберігайте скільки завгодно паролів на необмеженій кількості пристроїв, використовуючи Bitwarden для мобільних пристроїв, браузерів та комп'ютерів." } } diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index a2b6756785c..4693c93af9c 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "Chính sách quyền riêng tư" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "Lời nhắc mật khẩu không được giống mật khẩu của bạn" }, diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index adbc102c496..c8eaacc8c86 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -1188,16 +1188,16 @@ "message": "此密码将用于导出和导入此文件" }, "accountRestrictedOptionDescription": { - "message": "使用衍生自您账户的用户名和主密码的账户加密密钥,以加密此导出并限制只能导入到当前的 Bitwarden 账户。" + "message": "使用衍生自您账户用户名和主密码的加密密钥以加密此导出,并限制只能导入到当前 Bitwarden 账户。" }, "passwordProtectedOptionDescription": { - "message": "设置一个文件密码用来加密此导出,并使用此密码解密以导入到任意 Bitwarden 账户。" + "message": "设置一个文件密码以加密此导出,并使用此密码解密以导入到任意 Bitwarden 账户。" }, "exportTypeHeading": { "message": "导出类型" }, "accountRestricted": { - "message": "账户受限" + "message": "账户限制" }, "filePasswordAndConfirmFilePasswordDoNotMatch": { "message": "「文件密码」与「确认文件密码」不一致。" @@ -2280,7 +2280,7 @@ "message": "随时。" }, "byContinuingYouAgreeToThe": { - "message": "若继续,代表您同意" + "message": "若继续,表示您同意" }, "and": { "message": "和" @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "隐私政策" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "您的新密码不能与当前密码相同。" + }, "hintEqualsPassword": { "message": "您的密码提示不能与您的密码相同。" }, @@ -3342,7 +3345,7 @@ "message": "识别到弱密码且其出现在数据泄露中。请使用一个强且唯一的密码以保护您的账户。确定要使用这个密码吗?" }, "checkForBreaches": { - "message": "检查已知的数据泄露是否包含此密码。" + "message": "检查已知的数据泄露是否包含此密码" }, "important": { "message": "重要:" diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index d5d55725444..1ba3a2166dd 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -2297,6 +2297,9 @@ "privacyPolicy": { "message": "隱私權政策" }, + "yourNewPasswordCannotBeTheSameAsYourCurrentPassword": { + "message": "Your new password cannot be the same as your current password." + }, "hintEqualsPassword": { "message": "密碼提示不能與您的密碼相同。" }, From 5006a2954611374a44c00481a5748e0e929e95a9 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Fri, 11 Apr 2025 09:12:18 -0700 Subject: [PATCH 06/19] [PM-17516][PM-17617] - Remove old add-edit and attachments components (#14087)ew * remove unused components * re-add add-edit * re-delete add-edit --- .../src/app/shared/loose-components.module.ts | 12 - .../individual-vault/add-edit.component.html | 1112 ----------------- .../individual-vault/add-edit.component.ts | 315 ----- .../attachments.component.html | 120 -- .../individual-vault/attachments.component.ts | 67 - .../app/vault/org-vault/add-edit.component.ts | 138 -- .../vault/org-vault/attachments.component.ts | 105 -- 7 files changed, 1869 deletions(-) delete mode 100644 apps/web/src/app/vault/individual-vault/add-edit.component.html delete mode 100644 apps/web/src/app/vault/individual-vault/add-edit.component.ts delete mode 100644 apps/web/src/app/vault/individual-vault/attachments.component.html delete mode 100644 apps/web/src/app/vault/individual-vault/attachments.component.ts delete mode 100644 apps/web/src/app/vault/org-vault/add-edit.component.ts delete mode 100644 apps/web/src/app/vault/org-vault/attachments.component.ts diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index 5de56789cad..5dc34b3b5b1 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -57,13 +57,9 @@ import { WeakPasswordsReportComponent as OrgWeakPasswordsReportComponent } from /* eslint no-restricted-imports: "error" */ import { PremiumBadgeComponent } from "../vault/components/premium-badge.component"; import { AddEditCustomFieldsComponent } from "../vault/individual-vault/add-edit-custom-fields.component"; -import { AddEditComponent } from "../vault/individual-vault/add-edit.component"; -import { AttachmentsComponent } from "../vault/individual-vault/attachments.component"; import { FolderAddEditComponent } from "../vault/individual-vault/folder-add-edit.component"; import { OrganizationBadgeModule } from "../vault/individual-vault/organization-badge/organization-badge.module"; import { PipesModule } from "../vault/individual-vault/pipes/pipes.module"; -import { AddEditComponent as OrgAddEditComponent } from "../vault/org-vault/add-edit.component"; -import { AttachmentsComponent as OrgAttachmentsComponent } from "../vault/org-vault/attachments.component"; import { PurgeVaultComponent } from "../vault/settings/purge-vault.component"; import { EnvironmentSelectorModule } from "./../components/environment-selector/environment-selector.module"; @@ -97,11 +93,9 @@ import { SharedModule } from "./shared.module"; declarations: [ AcceptFamilySponsorshipComponent, AccountComponent, - AddEditComponent, AddEditCustomFieldsComponent, AddEditCustomFieldsComponent, ApiKeyComponent, - AttachmentsComponent, ChangeEmailComponent, DeauthorizeSessionsComponent, DeleteAccountDialogComponent, @@ -113,8 +107,6 @@ import { SharedModule } from "./shared.module"; EmergencyAccessViewComponent, FolderAddEditComponent, FrontendLayoutComponent, - OrgAddEditComponent, - OrgAttachmentsComponent, OrgEventsComponent, OrgExposedPasswordsReportComponent, OrgInactiveTwoFactorReportComponent, @@ -146,11 +138,9 @@ import { SharedModule } from "./shared.module"; UserVerificationModule, PremiumBadgeComponent, AccountComponent, - AddEditComponent, AddEditCustomFieldsComponent, AddEditCustomFieldsComponent, ApiKeyComponent, - AttachmentsComponent, ChangeEmailComponent, DeauthorizeSessionsComponent, DeleteAccountDialogComponent, @@ -163,9 +153,7 @@ import { SharedModule } from "./shared.module"; EmergencyAccessViewComponent, FolderAddEditComponent, FrontendLayoutComponent, - OrgAddEditComponent, OrganizationLayoutComponent, - OrgAttachmentsComponent, OrgEventsComponent, OrgExposedPasswordsReportComponent, OrgInactiveTwoFactorReportComponent, diff --git a/apps/web/src/app/vault/individual-vault/add-edit.component.html b/apps/web/src/app/vault/individual-vault/add-edit.component.html deleted file mode 100644 index 29de65efcd5..00000000000 --- a/apps/web/src/app/vault/individual-vault/add-edit.component.html +++ /dev/null @@ -1,1112 +0,0 @@ - diff --git a/apps/web/src/app/vault/individual-vault/add-edit.component.ts b/apps/web/src/app/vault/individual-vault/add-edit.component.ts deleted file mode 100644 index d8739465938..00000000000 --- a/apps/web/src/app/vault/individual-vault/add-edit.component.ts +++ /dev/null @@ -1,315 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { DatePipe } from "@angular/common"; -import { Component, OnDestroy, OnInit } from "@angular/core"; -import { firstValueFrom, map } from "rxjs"; - -import { CollectionService } from "@bitwarden/admin-console/common"; -import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/vault/components/add-edit.component"; -import { AuditService } from "@bitwarden/common/abstractions/audit.service"; -import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { isCardExpired } from "@bitwarden/common/autofill/utils"; -import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { ProductTierType } from "@bitwarden/common/billing/enums"; -import { EventType } from "@bitwarden/common/enums"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; -import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; -import { CipherType } from "@bitwarden/common/vault/enums"; -import { Launchable } from "@bitwarden/common/vault/interfaces/launchable"; -import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; -import { DialogService, ToastService } from "@bitwarden/components"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; -import { PasswordRepromptService, SshImportPromptService } from "@bitwarden/vault"; - -@Component({ - selector: "app-vault-add-edit", - templateUrl: "add-edit.component.html", -}) -export class AddEditComponent extends BaseAddEditComponent implements OnInit, OnDestroy { - canAccessPremium: boolean; - totpCode: string; - totpCodeFormatted: string; - totpDash: number; - totpSec: number; - totpLow: boolean; - showRevisionDate = false; - hasPasswordHistory = false; - viewingPasswordHistory = false; - viewOnly = false; - showPasswordCount = false; - cardIsExpired: boolean = false; - - protected totpInterval: number; - protected override componentName = "app-vault-add-edit"; - - constructor( - cipherService: CipherService, - folderService: FolderService, - i18nService: I18nService, - platformUtilsService: PlatformUtilsService, - auditService: AuditService, - accountService: AccountService, - collectionService: CollectionService, - protected totpService: TotpService, - protected passwordGenerationService: PasswordGenerationServiceAbstraction, - protected messagingService: MessagingService, - eventCollectionService: EventCollectionService, - protected policyService: PolicyService, - organizationService: OrganizationService, - logService: LogService, - passwordRepromptService: PasswordRepromptService, - dialogService: DialogService, - datePipe: DatePipe, - configService: ConfigService, - private billingAccountProfileStateService: BillingAccountProfileStateService, - cipherAuthorizationService: CipherAuthorizationService, - toastService: ToastService, - sdkService: SdkService, - sshImportPromptService: SshImportPromptService, - ) { - super( - cipherService, - folderService, - i18nService, - platformUtilsService, - auditService, - accountService, - collectionService, - messagingService, - eventCollectionService, - policyService, - logService, - passwordRepromptService, - organizationService, - dialogService, - window, - datePipe, - configService, - cipherAuthorizationService, - toastService, - sdkService, - sshImportPromptService, - ); - } - - async ngOnInit() { - await super.ngOnInit(); - await this.load(); - - this.viewOnly = !this.cipher.edit && this.editMode; - // remove when all the title for all clients are updated to New Item - if (this.cloneMode || !this.editMode) { - this.title = this.i18nService.t("newItem"); - } - this.showRevisionDate = this.cipher.passwordRevisionDisplayDate != null; - this.hasPasswordHistory = this.cipher.hasPasswordHistory; - this.cleanUp(); - - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a.id)), - ); - - this.canAccessPremium = await firstValueFrom( - this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId), - ); - - if (this.showTotp()) { - await this.totpUpdateCode(); - const totpResponse = await firstValueFrom(this.totpService.getCode$(this.cipher.login.totp)); - if (totpResponse) { - const interval = totpResponse.period; - await this.totpTick(interval); - - this.totpInterval = window.setInterval(async () => { - await this.totpTick(interval); - }, 1000); - } - } - - this.cardIsExpired = isCardExpired(this.cipher.card); - } - - ngOnDestroy() { - super.ngOnDestroy(); - } - - toggleFavorite() { - this.cipher.favorite = !this.cipher.favorite; - } - - togglePassword() { - super.togglePassword(); - - // Hide password count when password is hidden to be safe - if (!this.showPassword && this.showPasswordCount) { - this.togglePasswordCount(); - } - } - - togglePasswordCount() { - this.showPasswordCount = !this.showPasswordCount; - - if (this.editMode && this.showPasswordCount) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.eventCollectionService.collect( - EventType.Cipher_ClientToggledPasswordVisible, - this.cipherId, - ); - } - } - - launch(uri: Launchable) { - if (!uri.canLaunch) { - return; - } - - this.platformUtilsService.launchUri(uri.launchUri); - } - - async copy(value: string, typeI18nKey: string, aType: string): Promise { - if (value == null) { - return false; - } - - this.platformUtilsService.copyToClipboard(value, { window: window }); - this.toastService.showToast({ - variant: "info", - title: null, - message: this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)), - }); - - if (this.editMode) { - if (typeI18nKey === "password") { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.eventCollectionService.collect(EventType.Cipher_ClientCopiedPassword, this.cipherId); - } else if (typeI18nKey === "securityCode") { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.eventCollectionService.collect(EventType.Cipher_ClientCopiedCardCode, this.cipherId); - } else if (aType === "H_Field") { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.eventCollectionService.collect( - EventType.Cipher_ClientCopiedHiddenField, - this.cipherId, - ); - } - } - - return true; - } - - async generatePassword(): Promise { - const confirmed = await super.generatePassword(); - if (confirmed) { - const options = (await this.passwordGenerationService.getOptions())?.[0] ?? {}; - this.cipher.login.password = await this.passwordGenerationService.generatePassword(options); - } - return confirmed; - } - - premiumRequired() { - if (!this.canAccessPremium) { - this.messagingService.send("premiumRequired"); - return; - } - } - - upgradeOrganization() { - this.messagingService.send("upgradeOrganization", { - organizationId: this.cipher.organizationId, - }); - } - - showGetPremium() { - if (this.canAccessPremium) { - return; - } - if (this.cipher.organizationUseTotp) { - this.upgradeOrganization(); - } else { - this.premiumRequired(); - } - } - - viewHistory() { - this.viewingPasswordHistory = !this.viewingPasswordHistory; - } - - protected cleanUp() { - if (this.totpInterval) { - window.clearInterval(this.totpInterval); - } - } - - protected async totpUpdateCode() { - if ( - this.cipher == null || - this.cipher.type !== CipherType.Login || - this.cipher.login.totp == null - ) { - if (this.totpInterval) { - window.clearInterval(this.totpInterval); - } - return; - } - - const totpResponse = await firstValueFrom(this.totpService.getCode$(this.cipher.login.totp)); - this.totpCode = totpResponse?.code; - if (this.totpCode != null) { - if (this.totpCode.length > 4) { - const half = Math.floor(this.totpCode.length / 2); - this.totpCodeFormatted = - this.totpCode.substring(0, half) + " " + this.totpCode.substring(half); - } else { - this.totpCodeFormatted = this.totpCode; - } - } else { - this.totpCodeFormatted = null; - if (this.totpInterval) { - window.clearInterval(this.totpInterval); - } - } - } - - protected allowOwnershipAssignment() { - return ( - (!this.editMode || this.cloneMode) && - this.ownershipOptions != null && - (this.ownershipOptions.length > 1 || !this.allowPersonal) - ); - } - - protected showTotp() { - return ( - this.cipher.type === CipherType.Login && - this.cipher.login.totp && - this.organization?.productTierType != ProductTierType.Free && - (this.cipher.organizationUseTotp || this.canAccessPremium) - ); - } - - private async totpTick(intervalSeconds: number) { - const epoch = Math.round(new Date().getTime() / 1000.0); - const mod = epoch % intervalSeconds; - - this.totpSec = intervalSeconds - mod; - this.totpDash = +(Math.round(((78.6 / intervalSeconds) * mod + "e+2") as any) + "e-2"); - this.totpLow = this.totpSec <= 7; - if (mod === 0) { - await this.totpUpdateCode(); - } - } -} diff --git a/apps/web/src/app/vault/individual-vault/attachments.component.html b/apps/web/src/app/vault/individual-vault/attachments.component.html deleted file mode 100644 index d657cd7f6c3..00000000000 --- a/apps/web/src/app/vault/individual-vault/attachments.component.html +++ /dev/null @@ -1,120 +0,0 @@ - diff --git a/apps/web/src/app/vault/individual-vault/attachments.component.ts b/apps/web/src/app/vault/individual-vault/attachments.component.ts deleted file mode 100644 index c6079dbe78f..00000000000 --- a/apps/web/src/app/vault/individual-vault/attachments.component.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Component } from "@angular/core"; - -import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/angular/vault/components/attachments.component"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; -import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view"; -import { DialogService, ToastService } from "@bitwarden/components"; -import { KeyService } from "@bitwarden/key-management"; - -@Component({ - selector: "app-vault-attachments", - templateUrl: "attachments.component.html", -}) -export class AttachmentsComponent extends BaseAttachmentsComponent { - protected override componentName = "app-vault-attachments"; - - constructor( - cipherService: CipherService, - i18nService: I18nService, - keyService: KeyService, - encryptService: EncryptService, - stateService: StateService, - platformUtilsService: PlatformUtilsService, - apiService: ApiService, - logService: LogService, - fileDownloadService: FileDownloadService, - dialogService: DialogService, - billingAccountProfileStateService: BillingAccountProfileStateService, - accountService: AccountService, - toastService: ToastService, - ) { - super( - cipherService, - i18nService, - keyService, - encryptService, - platformUtilsService, - apiService, - window, - logService, - stateService, - fileDownloadService, - dialogService, - billingAccountProfileStateService, - accountService, - toastService, - ); - } - - protected async reupload(attachment: AttachmentView) { - if (this.showFixOldAttachments(attachment)) { - await this.reuploadCipherAttachment(attachment, false); - } - } - - protected showFixOldAttachments(attachment: AttachmentView) { - return attachment.key == null && this.cipher.organizationId == null; - } -} diff --git a/apps/web/src/app/vault/org-vault/add-edit.component.ts b/apps/web/src/app/vault/org-vault/add-edit.component.ts deleted file mode 100644 index 89f3b79f1fb..00000000000 --- a/apps/web/src/app/vault/org-vault/add-edit.component.ts +++ /dev/null @@ -1,138 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { DatePipe } from "@angular/common"; -import { Component } from "@angular/core"; -import { firstValueFrom } from "rxjs"; - -import { CollectionService } from "@bitwarden/admin-console/common"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { AuditService } from "@bitwarden/common/abstractions/audit.service"; -import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; -import { UserId } from "@bitwarden/common/types/guid"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; -import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; -import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; -import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; -import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; -import { DialogService, ToastService } from "@bitwarden/components"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; -import { PasswordRepromptService, SshImportPromptService } from "@bitwarden/vault"; - -import { AddEditComponent as BaseAddEditComponent } from "../individual-vault/add-edit.component"; - -@Component({ - selector: "app-org-vault-add-edit", - templateUrl: "../individual-vault/add-edit.component.html", -}) -export class AddEditComponent extends BaseAddEditComponent { - originalCipher: Cipher = null; - protected override componentName = "app-org-vault-add-edit"; - - constructor( - cipherService: CipherService, - folderService: FolderService, - i18nService: I18nService, - platformUtilsService: PlatformUtilsService, - auditService: AuditService, - accountService: AccountService, - collectionService: CollectionService, - totpService: TotpService, - passwordGenerationService: PasswordGenerationServiceAbstraction, - private apiService: ApiService, - messagingService: MessagingService, - eventCollectionService: EventCollectionService, - policyService: PolicyService, - logService: LogService, - passwordRepromptService: PasswordRepromptService, - organizationService: OrganizationService, - dialogService: DialogService, - datePipe: DatePipe, - configService: ConfigService, - billingAccountProfileStateService: BillingAccountProfileStateService, - cipherAuthorizationService: CipherAuthorizationService, - toastService: ToastService, - sdkService: SdkService, - sshImportPromptService: SshImportPromptService, - ) { - super( - cipherService, - folderService, - i18nService, - platformUtilsService, - auditService, - accountService, - collectionService, - totpService, - passwordGenerationService, - messagingService, - eventCollectionService, - policyService, - organizationService, - logService, - passwordRepromptService, - dialogService, - datePipe, - configService, - billingAccountProfileStateService, - cipherAuthorizationService, - toastService, - sdkService, - sshImportPromptService, - ); - } - - protected loadCollections() { - if (!this.organization.canEditAllCiphers) { - return super.loadCollections(); - } - return Promise.resolve(this.collections); - } - - protected async loadCipher() { - this.isAdminConsoleAction = true; - const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - // Calling loadCipher first to assess if the cipher is unassigned. If null use apiService getCipherAdmin - const firstCipherCheck = await super.loadCipher(activeUserId); - - if (!this.organization.canEditAllCiphers && firstCipherCheck != null) { - return firstCipherCheck; - } - const response = await this.apiService.getCipherAdmin(this.cipherId); - const data = new CipherData(response); - - data.edit = true; - const cipher = new Cipher(data); - this.originalCipher = cipher; - return cipher; - } - - protected encryptCipher(userId: UserId) { - if (!this.organization.canEditAllCiphers) { - return super.encryptCipher(userId); - } - - return this.cipherService.encrypt(this.cipher, userId, null, null, this.originalCipher); - } - - protected async deleteCipher() { - if (!this.organization.canEditAllCiphers) { - const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - return super.deleteCipher(activeUserId); - } - return this.cipher.isDeleted - ? this.apiService.deleteCipherAdmin(this.cipherId) - : this.apiService.putDeleteCipherAdmin(this.cipherId); - } -} diff --git a/apps/web/src/app/vault/org-vault/attachments.component.ts b/apps/web/src/app/vault/org-vault/attachments.component.ts deleted file mode 100644 index c2ad82bc27a..00000000000 --- a/apps/web/src/app/vault/org-vault/attachments.component.ts +++ /dev/null @@ -1,105 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, OnInit } from "@angular/core"; -import { firstValueFrom } from "rxjs"; - -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; -import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { UserId } from "@bitwarden/common/types/guid"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; -import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; -import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view"; -import { DialogService, ToastService } from "@bitwarden/components"; -import { KeyService } from "@bitwarden/key-management"; - -import { AttachmentsComponent as BaseAttachmentsComponent } from "../individual-vault/attachments.component"; - -@Component({ - selector: "app-org-vault-attachments", - templateUrl: "../individual-vault/attachments.component.html", -}) -export class AttachmentsComponent extends BaseAttachmentsComponent implements OnInit { - viewOnly = false; - organization: Organization; - - constructor( - cipherService: CipherService, - i18nService: I18nService, - keyService: KeyService, - encryptService: EncryptService, - stateService: StateService, - platformUtilsService: PlatformUtilsService, - apiService: ApiService, - logService: LogService, - fileDownloadService: FileDownloadService, - dialogService: DialogService, - billingAccountProfileStateService: BillingAccountProfileStateService, - accountService: AccountService, - toastService: ToastService, - ) { - super( - cipherService, - i18nService, - keyService, - encryptService, - stateService, - platformUtilsService, - apiService, - logService, - fileDownloadService, - dialogService, - billingAccountProfileStateService, - accountService, - toastService, - ); - } - - async ngOnInit() { - await super.ngOnInit(); - } - - protected async reupload(attachment: AttachmentView) { - if (this.organization.canEditAllCiphers && this.showFixOldAttachments(attachment)) { - await super.reuploadCipherAttachment(attachment, true); - } - } - - protected async loadCipher() { - if (!this.organization.canEditAllCiphers) { - const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - return await super.loadCipher(activeUserId); - } - const response = await this.apiService.getCipherAdmin(this.cipherId); - return new Cipher(new CipherData(response)); - } - - protected saveCipherAttachment(file: File, userId: UserId) { - return this.cipherService.saveAttachmentWithServer( - this.cipherDomain, - file, - userId, - this.organization.canEditAllCiphers, - ); - } - - protected deleteCipherAttachment(attachmentId: string, userId: UserId) { - if (!this.organization.canEditAllCiphers) { - return super.deleteCipherAttachment(attachmentId, userId); - } - return this.apiService.deleteCipherAttachmentAdmin(this.cipherId, attachmentId); - } - - protected showFixOldAttachments(attachment: AttachmentView) { - return attachment.key == null && this.organization.canEditAllCiphers; - } -} From d5b7af75e9b0083902654a7c5b06f41a53ad4b4f Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Fri, 11 Apr 2025 15:16:30 -0400 Subject: [PATCH 07/19] [PM-14909] Build components for security task completion notification (#14230) * squash split component work from pm-14909 * fix typing --- apps/browser/src/_locales/en/messages.json | 25 ++ .../components/buttons/action-button.ts | 9 +- .../content/components/icons/external-link.ts | 22 ++ .../content/components/icons/index.ts | 2 + .../content/components/icons/keyhole.ts | 222 ++++++++++++++++++ .../lit-stories/icons/icons.lit-stories.ts | 2 + .../notification/body.lit-stories.ts | 2 +- .../body.lit-stories.ts} | 26 +- .../confirmation/container.lit-stories.ts | 55 +++++ .../confirmation/footer.lit-stories.ts | 36 +++ .../confirmation/message.lit-stories.ts | 37 +++ .../notification/container.lit-stories.ts | 61 +++++ .../notification/footer.lit-stories.ts | 2 +- .../notification/header.lit-stories.ts | 2 +- .../notification/confirmation-message.ts | 53 ----- .../{confirmation.ts => confirmation/body.ts} | 48 ++-- .../container.ts} | 69 ++++-- .../notification/confirmation/footer.ts | 59 +++++ .../notification/confirmation/message.ts | 83 +++++++ .../components/notification/container.ts | 24 +- .../abstractions/notification-bar.ts | 6 + apps/browser/src/autofill/notification/bar.ts | 6 +- 22 files changed, 730 insertions(+), 121 deletions(-) create mode 100644 apps/browser/src/autofill/content/components/icons/external-link.ts create mode 100644 apps/browser/src/autofill/content/components/icons/keyhole.ts rename apps/browser/src/autofill/content/components/lit-stories/notification/{confirmation.lit-stories.ts => confirmation/body.lit-stories.ts} (53%) create mode 100644 apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/container.lit-stories.ts create mode 100644 apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/footer.lit-stories.ts create mode 100644 apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/message.lit-stories.ts create mode 100644 apps/browser/src/autofill/content/components/lit-stories/notification/container.lit-stories.ts delete mode 100644 apps/browser/src/autofill/content/components/notification/confirmation-message.ts rename apps/browser/src/autofill/content/components/notification/{confirmation.ts => confirmation/body.ts} (63%) rename apps/browser/src/autofill/content/components/notification/{confirmation-container.ts => confirmation/container.ts} (66%) create mode 100644 apps/browser/src/autofill/content/components/notification/confirmation/footer.ts create mode 100644 apps/browser/src/autofill/content/components/notification/confirmation/message.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 586e7e1f2cf..f3b85496b75 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/autofill/content/components/buttons/action-button.ts b/apps/browser/src/autofill/content/components/buttons/action-button.ts index f0642d4233a..881b44b4785 100644 --- a/apps/browser/src/autofill/content/components/buttons/action-button.ts +++ b/apps/browser/src/autofill/content/components/buttons/action-button.ts @@ -1,5 +1,5 @@ import { css } from "@emotion/css"; -import { html } from "lit"; +import { html, TemplateResult } from "lit"; import { Theme } from "@bitwarden/common/platform/enums"; @@ -11,7 +11,7 @@ export function ActionButton({ theme, handleClick, }: { - buttonText: string; + buttonText: string | TemplateResult; disabled?: boolean; theme: Theme; handleClick: (e: Event) => void; @@ -63,4 +63,9 @@ const actionButtonStyles = ({ disabled, theme }: { disabled: boolean; theme: The color: ${themes[theme].text.contrast}; } `} + + svg { + width: fit-content; + height: 16px; + } `; diff --git a/apps/browser/src/autofill/content/components/icons/external-link.ts b/apps/browser/src/autofill/content/components/icons/external-link.ts new file mode 100644 index 00000000000..10c6d831025 --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/external-link.ts @@ -0,0 +1,22 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { IconProps } from "../common-types"; +import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; + +export function ExternalLink({ color, disabled, theme }: IconProps) { + const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; + + return html` + + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/icons/index.ts b/apps/browser/src/autofill/content/components/icons/index.ts index c4769a0e69d..4b6cb7abdd8 100644 --- a/apps/browser/src/autofill/content/components/icons/index.ts +++ b/apps/browser/src/autofill/content/components/icons/index.ts @@ -4,9 +4,11 @@ export { BrandIconContainer } from "./brand-icon-container"; export { Business } from "./business"; export { Close } from "./close"; export { ExclamationTriangle } from "./exclamation-triangle"; +export { ExternalLink } from "./external-link"; export { Family } from "./family"; export { Folder } from "./folder"; export { Globe } from "./globe"; +export { Keyhole } from "./keyhole"; export { PartyHorn } from "./party-horn"; export { PencilSquare } from "./pencil-square"; export { Shield } from "./shield"; diff --git a/apps/browser/src/autofill/content/components/icons/keyhole.ts b/apps/browser/src/autofill/content/components/icons/keyhole.ts new file mode 100644 index 00000000000..0294c0c8499 --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/keyhole.ts @@ -0,0 +1,222 @@ +import { html } from "lit"; + +import { ThemeTypes } from "@bitwarden/common/platform/enums"; + +import { IconProps } from "../common-types"; + +// This icon has static multi-colors for each theme +export function Keyhole({ theme }: IconProps) { + if (theme === ThemeTypes.Dark) { + return html` + + + + + + + + + + + + + + + + + + + + + + `; + } + + return html` + + + + + + + + + + + + + + + + + + + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts index 20c88a59246..8bd87ef6674 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts +++ b/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts @@ -57,9 +57,11 @@ export const BusinessIcon = createIconStory("Business"); export const BrandIcon = createIconStory("BrandIconContainer"); export const CloseIcon = createIconStory("Close"); export const ExclamationTriangleIcon = createIconStory("ExclamationTriangle"); +export const ExternalLinkIcon = createIconStory("ExternalLink"); export const FamilyIcon = createIconStory("Family"); export const FolderIcon = createIconStory("Folder"); export const GlobeIcon = createIconStory("Globe"); +export const KeyholeIcon = createIconStory("Keyhole"); export const PartyHornIcon = createIconStory("PartyHorn"); export const PencilSquareIcon = createIconStory("PencilSquare"); export const ShieldIcon = createIconStory("Shield"); diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts index e43bc08b920..32b4170d1da 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts @@ -16,7 +16,7 @@ type Args = { }; export default { - title: "Components/Notifications/Notification Body", + title: "Components/Notifications/Body", argTypes: { ciphers: { control: "object" }, theme: { control: "select", options: [...Object.values(ThemeTypes)] }, diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/body.lit-stories.ts similarity index 53% rename from apps/browser/src/autofill/content/components/lit-stories/notification/confirmation.lit-stories.ts rename to apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/body.lit-stories.ts index b3dee95efd0..4d9be06fd7e 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation.lit-stories.ts +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/body.lit-stories.ts @@ -1,29 +1,26 @@ import { Meta, StoryObj } from "@storybook/web-components"; -import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; +import { ThemeTypes } from "@bitwarden/common/platform/enums"; -import { NotificationConfirmationBody } from "../../notification/confirmation"; - -type Args = { - buttonText: string; - confirmationMessage: string; - handleOpenVault: () => void; - theme: Theme; - error: string; -}; +import { + NotificationConfirmationBody, + NotificationConfirmationBodyProps, +} from "../../../notification/confirmation/body"; export default { - title: "Components/Notifications/Notification Confirmation Body", + title: "Components/Notifications/Confirmation/Body", argTypes: { error: { control: "text" }, buttonText: { control: "text" }, confirmationMessage: { control: "text" }, + messageDetails: { control: "text" }, theme: { control: "select", options: [...Object.values(ThemeTypes)] }, }, args: { error: "", buttonText: "View", confirmationMessage: "[item name] updated in Bitwarden.", + messageDetails: "You can view it in your vault.", theme: ThemeTypes.Light, }, parameters: { @@ -32,10 +29,11 @@ export default { url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=485-20160&m=dev", }, }, -} as Meta; +} as Meta; -const Template = (args: Args) => NotificationConfirmationBody({ ...args }); +const Template = (args: NotificationConfirmationBodyProps) => + NotificationConfirmationBody({ ...args }); -export const Default: StoryObj = { +export const Default: StoryObj = { render: Template, }; diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/container.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/container.lit-stories.ts new file mode 100644 index 00000000000..ec7194004d8 --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/container.lit-stories.ts @@ -0,0 +1,55 @@ +import { Meta, StoryObj } from "@storybook/web-components"; + +import { ThemeTypes } from "@bitwarden/common/platform/enums"; + +import { NotificationTypes } from "../../../../../notification/abstractions/notification-bar"; +import { + NotificationConfirmationContainer, + NotificationConfirmationContainerProps, +} from "../../../notification/confirmation/container"; + +export default { + title: "Components/Notifications/Confirmation", + argTypes: { + error: { control: "text" }, + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + type: { control: "select", options: [NotificationTypes.Change, NotificationTypes.Add] }, + }, + args: { + error: "", + task: { + orgName: "Acme, Inc.", + remainingTasksCount: 0, + }, + handleCloseNotification: () => alert("Close notification action triggered"), + handleOpenTasks: () => alert("Open tasks action triggered"), + i18n: { + loginSaveSuccess: "Login saved", + loginUpdateSuccess: "Login updated", + loginUpdateTaskSuccessAdditional: + "Thank you for making your organization more secure. You have 3 more passwords to update.", + loginUpdateTaskSuccess: + "Great job! You took the steps to make you and your organization more secure.", + nextSecurityTaskAction: "Change next password", + saveFailure: "Error saving", + saveFailureDetails: "Oh no! We couldn't save this. Try entering the details manually.", + view: "View", + }, + type: NotificationTypes.Change, + username: "Acme, Inc. Login", + theme: ThemeTypes.Light, + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=485-20160&m=dev", + }, + }, +} as Meta; + +const Template = (args: NotificationConfirmationContainerProps) => + NotificationConfirmationContainer({ ...args }); + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/footer.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/footer.lit-stories.ts new file mode 100644 index 00000000000..953fb90d067 --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/footer.lit-stories.ts @@ -0,0 +1,36 @@ +import { Meta, StoryObj } from "@storybook/web-components"; +import { html } from "lit"; + +import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; + +import { + NotificationConfirmationFooter, + NotificationConfirmationFooterProps, +} from "../../../notification/confirmation/footer"; + +export default { + title: "Components/Notifications/Confirmation/Footer", + argTypes: { + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + }, + args: { + handleButtonClick: () => alert("Action button triggered"), + i18n: { + nextSecurityTaskAction: "Change next password", + }, + theme: ThemeTypes.Light, + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=32-4949&m=dev", + }, + }, +} as Meta; + +const Template = (args: NotificationConfirmationFooterProps) => + html`
${NotificationConfirmationFooter({ ...args })}
`; + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/message.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/message.lit-stories.ts new file mode 100644 index 00000000000..f01503b331f --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/message.lit-stories.ts @@ -0,0 +1,37 @@ +import { Meta, StoryObj } from "@storybook/web-components"; + +import { ThemeTypes } from "@bitwarden/common/platform/enums"; + +import { + NotificationConfirmationMessage, + NotificationConfirmationMessageProps, +} from "../../../notification/confirmation/message"; + +export default { + title: "Components/Notifications/Confirmation/Message", + argTypes: { + buttonText: { control: "text" }, + message: { control: "text" }, + messageDetails: { control: "text" }, + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + }, + args: { + buttonText: "View", + message: "[item name] updated in Bitwarden.", + messageDetails: "It was added to your vault.", + theme: ThemeTypes.Light, + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=485-20160&m=dev", + }, + }, +} as Meta; + +const Template = (args: NotificationConfirmationMessageProps) => + NotificationConfirmationMessage({ ...args }); + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/container.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/container.lit-stories.ts new file mode 100644 index 00000000000..351c971ec0e --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/container.lit-stories.ts @@ -0,0 +1,61 @@ +import { Meta, StoryObj } from "@storybook/web-components"; + +import { ThemeTypes } from "@bitwarden/common/platform/enums"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; + +import { NotificationTypes } from "../../../../notification/abstractions/notification-bar"; +import { NotificationContainer, NotificationContainerProps } from "../../notification/container"; + +export default { + title: "Components/Notifications", + argTypes: { + error: { control: "text" }, + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + type: { control: "select", options: [...Object.values(NotificationTypes)] }, + }, + args: { + error: "", + ciphers: [ + { + id: "1", + name: "Example Cipher", + type: CipherType.Login, + favorite: false, + reprompt: CipherRepromptType.None, + icon: { + imageEnabled: true, + image: "", + fallbackImage: "https://example.com/fallback.png", + icon: "icon-class", + }, + login: { username: "user@example.com" }, + }, + ], + i18n: { + loginSaveSuccess: "Login saved", + loginUpdateSuccess: "Login updated", + saveAction: "Save", + saveAsNewLoginAction: "Save as new login", + saveFailure: "Error saving", + saveFailureDetails: "Oh no! We couldn't save this. Try entering the details manually.", + updateLoginPrompt: "Update existing login?", + view: "View", + }, + type: NotificationTypes.Change, + username: "mockUsername", + theme: ThemeTypes.Light, + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=485-20160&m=dev", + }, + }, +} as Meta; + +const Template = (args: NotificationContainerProps) => NotificationContainer({ ...args }); + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/footer.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/footer.lit-stories.ts index ea2bbdc2e15..29d9955ec64 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/notification/footer.lit-stories.ts +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/footer.lit-stories.ts @@ -7,7 +7,7 @@ import { NotificationFooter, NotificationFooterProps } from "../../notification/ import { mockFolderData, mockOrganizationData } from "../mock-data"; export default { - title: "Components/Notifications/Notification Footer", + title: "Components/Notifications/Footer", argTypes: { notificationType: { control: "select", diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/header.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/header.lit-stories.ts index 49cc1e6bd8d..0857c99130e 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/notification/header.lit-stories.ts +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/header.lit-stories.ts @@ -12,7 +12,7 @@ type Args = { }; export default { - title: "Components/Notifications/Notification Header", + title: "Components/Notifications/Header", argTypes: { message: { control: "text" }, standalone: { control: "boolean" }, diff --git a/apps/browser/src/autofill/content/components/notification/confirmation-message.ts b/apps/browser/src/autofill/content/components/notification/confirmation-message.ts deleted file mode 100644 index d6f7ba3024d..00000000000 --- a/apps/browser/src/autofill/content/components/notification/confirmation-message.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { css } from "@emotion/css"; -import { html } from "lit"; - -import { Theme } from "@bitwarden/common/platform/enums"; - -import { themes } from "../constants/styles"; - -export function NotificationConfirmationMessage({ - buttonText, - confirmationMessage, - handleClick, - theme, -}: { - buttonText: string; - confirmationMessage: string; - handleClick: (e: Event) => void; - theme: Theme; -}) { - return html` - ${confirmationMessage} - ${buttonText} - `; -} - -const baseTextStyles = css` - flex-grow: 1; - overflow-x: hidden; - text-align: left; - text-overflow: ellipsis; - line-height: 24px; - font-family: "DM Sans", sans-serif; - font-size: 16px; -`; - -const notificationConfirmationMessageStyles = (theme: Theme) => css` - ${baseTextStyles} - color: ${themes[theme].text.main}; - font-weight: 400; -`; - -const notificationConfirmationButtonTextStyles = (theme: Theme) => css` - ${baseTextStyles} - color: ${themes[theme].primary[600]}; - font-weight: 700; - cursor: pointer; -`; diff --git a/apps/browser/src/autofill/content/components/notification/confirmation.ts b/apps/browser/src/autofill/content/components/notification/confirmation/body.ts similarity index 63% rename from apps/browser/src/autofill/content/components/notification/confirmation.ts rename to apps/browser/src/autofill/content/components/notification/confirmation/body.ts index 8c213a7663f..55d257b36f4 100644 --- a/apps/browser/src/autofill/content/components/notification/confirmation.ts +++ b/apps/browser/src/autofill/content/components/notification/confirmation/body.ts @@ -1,12 +1,12 @@ import createEmotion from "@emotion/css/create-instance"; -import { html } from "lit"; +import { html, nothing } from "lit"; import { Theme } from "@bitwarden/common/platform/enums"; -import { themes } from "../constants/styles"; -import { PartyHorn, Warning } from "../icons"; +import { themes } from "../../constants/styles"; +import { PartyHorn, Keyhole, Warning } from "../../icons"; -import { NotificationConfirmationMessage } from "./confirmation-message"; +import { NotificationConfirmationMessage } from "./message"; export const componentClassPrefix = "notification-confirmation-body"; @@ -14,31 +14,41 @@ const { css } = createEmotion({ key: componentClassPrefix, }); -export function NotificationConfirmationBody({ - buttonText, - error, - confirmationMessage, - theme, - handleOpenVault, -}: { - error?: string; +export type NotificationConfirmationBodyProps = { buttonText: string; confirmationMessage: string; + error?: string; + messageDetails?: string; + tasksAreComplete?: boolean; theme: Theme; handleOpenVault: (e: Event) => void; -}) { - const IconComponent = !error ? PartyHorn : Warning; +}; + +export function NotificationConfirmationBody({ + buttonText, + confirmationMessage, + error, + messageDetails, + tasksAreComplete, + theme, + handleOpenVault, +}: NotificationConfirmationBodyProps) { + const IconComponent = tasksAreComplete ? Keyhole : !error ? PartyHorn : Warning; + + const showConfirmationMessage = confirmationMessage || buttonText || messageDetails; + return html`
${IconComponent({ theme })}
- ${confirmationMessage && buttonText + ${showConfirmationMessage ? NotificationConfirmationMessage({ - handleClick: handleOpenVault, - confirmationMessage, - theme, buttonText, + message: confirmationMessage, + messageDetails, + theme, + handleClick: handleOpenVault, }) - : null} + : nothing}
`; } diff --git a/apps/browser/src/autofill/content/components/notification/confirmation-container.ts b/apps/browser/src/autofill/content/components/notification/confirmation/container.ts similarity index 66% rename from apps/browser/src/autofill/content/components/notification/confirmation-container.ts rename to apps/browser/src/autofill/content/components/notification/confirmation/container.ts index 0666859ac44..a071338af9a 100644 --- a/apps/browser/src/autofill/content/components/notification/confirmation-container.ts +++ b/apps/browser/src/autofill/content/components/notification/confirmation/container.ts @@ -1,42 +1,67 @@ import { css } from "@emotion/css"; -import { html } from "lit"; +import { html, nothing } from "lit"; import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; import { NotificationBarIframeInitData, - NotificationTypes, + NotificationTaskInfo, NotificationType, -} from "../../../notification/abstractions/notification-bar"; -import { themes, spacing } from "../constants/styles"; - -import { NotificationConfirmationBody } from "./confirmation"; + NotificationTypes, +} from "../../../../notification/abstractions/notification-bar"; +import { themes, spacing } from "../../constants/styles"; import { NotificationHeader, componentClassPrefix as notificationHeaderClassPrefix, -} from "./header"; +} from "../header"; + +import { NotificationConfirmationBody } from "./body"; +import { NotificationConfirmationFooter } from "./footer"; + +export type NotificationConfirmationContainerProps = NotificationBarIframeInitData & { + handleCloseNotification: (e: Event) => void; + handleOpenVault: (e: Event) => void; + handleOpenTasks: (e: Event) => void; +} & { + error?: string; + i18n: { [key: string]: string }; + task?: NotificationTaskInfo; + type: NotificationType; + username: string; +}; export function NotificationConfirmationContainer({ error, handleCloseNotification, handleOpenVault, + handleOpenTasks, i18n, + task, theme = ThemeTypes.Light, type, username, -}: NotificationBarIframeInitData & { - handleCloseNotification: (e: Event) => void; - handleOpenVault: (e: Event) => void; -} & { - error?: string; - i18n: { [key: string]: string }; - type: NotificationType; - username: string; -}) { +}: NotificationConfirmationContainerProps) { const headerMessage = getHeaderMessage(i18n, type, error); const confirmationMessage = getConfirmationMessage(i18n, username, type, error); const buttonText = error ? i18n.newItem : i18n.view; + let messageDetails: string | undefined; + let remainingTasksCount: number | undefined; + let tasksAreComplete: boolean = false; + + if (task) { + remainingTasksCount = task.remainingTasksCount || 0; + tasksAreComplete = remainingTasksCount === 0; + + messageDetails = + remainingTasksCount > 0 + ? chrome.i18n.getMessage("loginUpdateTaskSuccessAdditional", [ + task.orgName, + `${remainingTasksCount}`, + ]) + : chrome.i18n.getMessage("loginUpdateTaskSuccess", [task.orgName]); + } + return html`
${NotificationHeader({ @@ -47,10 +72,18 @@ export function NotificationConfirmationContainer({ ${NotificationConfirmationBody({ buttonText, confirmationMessage, - error: error, - handleOpenVault, + tasksAreComplete, + messageDetails, theme, + handleOpenVault, })} + ${remainingTasksCount + ? NotificationConfirmationFooter({ + i18n, + theme, + handleButtonClick: handleOpenTasks, + }) + : nothing}
`; } diff --git a/apps/browser/src/autofill/content/components/notification/confirmation/footer.ts b/apps/browser/src/autofill/content/components/notification/confirmation/footer.ts new file mode 100644 index 00000000000..e245d22c8e8 --- /dev/null +++ b/apps/browser/src/autofill/content/components/notification/confirmation/footer.ts @@ -0,0 +1,59 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { ActionButton } from "../../buttons/action-button"; +import { spacing, themes } from "../../constants/styles"; +import { ExternalLink } from "../../icons"; + +export type NotificationConfirmationFooterProps = { + i18n: { [key: string]: string }; + theme: Theme; + handleButtonClick: (event: Event) => void; +}; + +export function NotificationConfirmationFooter({ + i18n, + theme, + handleButtonClick, +}: NotificationConfirmationFooterProps) { + const primaryButtonText = i18n.nextSecurityTaskAction; + + return html` +
+ ${ActionButton({ + handleClick: handleButtonClick, + buttonText: AdditionalTasksButtonContent({ buttonText: primaryButtonText, theme }), + theme, + })} +
+ `; +} + +const notificationConfirmationFooterStyles = ({ theme }: { theme: Theme }) => css` + background-color: ${themes[theme].background.alt}; + padding: 0 ${spacing[3]} ${spacing[3]} ${spacing[3]}; + max-width: min-content; + + :last-child { + border-radius: 0 0 ${spacing["4"]} ${spacing["4"]}; + padding-bottom: ${spacing[4]}; + } +`; + +function AdditionalTasksButtonContent({ buttonText, theme }: { buttonText: string; theme: Theme }) { + return html` +
+ ${buttonText} + ${ExternalLink({ theme, color: themes[theme].text.contrast })} +
+ `; +} + +const additionalTasksButtonContentStyles = ({ theme }: { theme: Theme }) => css` + gap: ${spacing[2]}; + display: flex; + align-items: center; + white-space: nowrap; +`; diff --git a/apps/browser/src/autofill/content/components/notification/confirmation/message.ts b/apps/browser/src/autofill/content/components/notification/confirmation/message.ts new file mode 100644 index 00000000000..c018371caff --- /dev/null +++ b/apps/browser/src/autofill/content/components/notification/confirmation/message.ts @@ -0,0 +1,83 @@ +import { css } from "@emotion/css"; +import { html, nothing } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { themes, typography } from "../../constants/styles"; + +export type NotificationConfirmationMessageProps = { + buttonText?: string; + message?: string; + messageDetails?: string; + handleClick: (e: Event) => void; + theme: Theme; +}; + +export function NotificationConfirmationMessage({ + buttonText, + message, + messageDetails, + handleClick, + theme, +}: NotificationConfirmationMessageProps) { + return html` +
+ ${message || buttonText + ? html` + + ${message || nothing} + ${buttonText + ? html` + + ${buttonText} + + ` + : nothing} + + ` + : nothing} + ${messageDetails + ? html`
${messageDetails}
` + : nothing} +
+ `; +} + +const baseTextStyles = css` + flex-grow: 1; + overflow-x: hidden; + text-align: left; + text-overflow: ellipsis; + line-height: 24px; + font-family: "DM Sans", sans-serif; + font-size: 16px; +`; + +const notificationConfirmationMessageStyles = (theme: Theme) => css` + ${baseTextStyles} + + color: ${themes[theme].text.main}; + font-weight: 400; +`; + +const notificationConfirmationButtonTextStyles = (theme: Theme) => css` + ${baseTextStyles} + + color: ${themes[theme].primary[600]}; + font-weight: 700; + cursor: pointer; +`; + +const AdditionalMessageStyles = ({ theme }: { theme: Theme }) => css` + ${typography.body2} + + font-size: 14px; + color: ${themes[theme].text.muted}; +`; diff --git a/apps/browser/src/autofill/content/components/notification/container.ts b/apps/browser/src/autofill/content/components/notification/container.ts index 8d80dc9fb50..c29f58e116b 100644 --- a/apps/browser/src/autofill/content/components/notification/container.ts +++ b/apps/browser/src/autofill/content/components/notification/container.ts @@ -19,6 +19,18 @@ import { componentClassPrefix as notificationHeaderClassPrefix, } from "./header"; +export type NotificationContainerProps = NotificationBarIframeInitData & { + handleCloseNotification: (e: Event) => void; + handleSaveAction: (e: Event) => void; + handleEditOrUpdateAction: (e: Event) => void; +} & { + ciphers?: NotificationCipherData[]; + folders?: FolderView[]; + i18n: { [key: string]: string }; + organizations?: OrgView[]; + type: NotificationType; // @TODO typing override for generic `NotificationBarIframeInitData.type` +}; + export function NotificationContainer({ handleCloseNotification, handleEditOrUpdateAction, @@ -29,17 +41,7 @@ export function NotificationContainer({ organizations, theme = ThemeTypes.Light, type, -}: NotificationBarIframeInitData & { - handleCloseNotification: (e: Event) => void; - handleSaveAction: (e: Event) => void; - handleEditOrUpdateAction: (e: Event) => void; -} & { - ciphers?: NotificationCipherData[]; - folders?: FolderView[]; - i18n: { [key: string]: string }; - organizations?: OrgView[]; - type: NotificationType; // @TODO typing override for generic `NotificationBarIframeInitData.type` -}) { +}: NotificationContainerProps) { const headerMessage = getHeaderMessage(i18n, type); const showBody = true; diff --git a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts index c138776ed0e..7e2fdab04d3 100644 --- a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts +++ b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts @@ -11,6 +11,11 @@ const NotificationTypes = { type NotificationType = (typeof NotificationTypes)[keyof typeof NotificationTypes]; +type NotificationTaskInfo = { + orgName: string; + remainingTasksCount: number; +}; + type NotificationBarIframeInitData = { ciphers?: NotificationCipherData[]; folders?: FolderView[]; @@ -38,6 +43,7 @@ type NotificationBarWindowMessageHandlers = { }; export { + NotificationTaskInfo, NotificationTypes, NotificationType, NotificationBarIframeInitData, diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts index 139b4551a24..f544e75527c 100644 --- a/apps/browser/src/autofill/notification/bar.ts +++ b/apps/browser/src/autofill/notification/bar.ts @@ -7,7 +7,7 @@ import type { FolderView } from "@bitwarden/common/vault/models/view/folder.view import { AdjustNotificationBarMessageData } from "../background/abstractions/notification.background"; import { NotificationCipherData } from "../content/components/cipher/types"; import { OrgView } from "../content/components/common-types"; -import { NotificationConfirmationContainer } from "../content/components/notification/confirmation-container"; +import { NotificationConfirmationContainer } from "../content/components/notification/confirmation/container"; import { NotificationContainer } from "../content/components/notification/container"; import { buildSvgDomElement } from "../utils"; import { circleCheckIcon } from "../utils/svg-icons"; @@ -58,6 +58,9 @@ function getI18n() { loginSaveSuccessDetails: chrome.i18n.getMessage("loginSaveSuccessDetails"), loginUpdateSuccess: chrome.i18n.getMessage("loginUpdateSuccess"), loginUpdateSuccessDetails: chrome.i18n.getMessage("loginUpdatedSuccessDetails"), + loginUpdateTaskSuccess: chrome.i18n.getMessage("loginUpdateTaskSuccess"), + loginUpdateTaskSuccessAdditional: chrome.i18n.getMessage("loginUpdateTaskSuccessAdditional"), + nextSecurityTaskAction: chrome.i18n.getMessage("nextSecurityTaskAction"), newItem: chrome.i18n.getMessage("newItem"), never: chrome.i18n.getMessage("never"), notificationAddDesc: chrome.i18n.getMessage("notificationAddDesc"), @@ -369,6 +372,7 @@ function handleSaveCipherConfirmation(message: NotificationBarWindowMessage) { error, username: username ?? i18n.typeLogin, handleOpenVault: (e) => cipherId && openViewVaultItemPopout(e, cipherId), + handleOpenTasks: () => {}, }), document.body, ); From 2fd83f830db77960367b2ddbf427bb9fed97e74f Mon Sep 17 00:00:00 2001 From: Jakub Gilis Date: Fri, 11 Apr 2025 21:30:06 +0200 Subject: [PATCH 08/19] Properly handle message aborts during cleanup (#13841) Replace the FallbackRequestedError rejection pattern with direct AbortController.abort() calls when destroying the Messenger. This eliminates misleading console errors and ensures correct cancellation behavior. The FallbackRequestedError is intended specifically for user-requested WebAuthn fallbacks, not general message cleanup operations. Fixes GitHub issue #12663 --- .../autofill/fido2/content/messaging/messenger.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/apps/browser/src/autofill/fido2/content/messaging/messenger.ts b/apps/browser/src/autofill/fido2/content/messaging/messenger.ts index a5530d87a8e..ec7ff3bb7a4 100644 --- a/apps/browser/src/autofill/fido2/content/messaging/messenger.ts +++ b/apps/browser/src/autofill/fido2/content/messaging/messenger.ts @@ -1,7 +1,5 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { FallbackRequestedError } from "@bitwarden/common/platform/abstractions/fido2/fido2-client.service.abstraction"; - import { Message, MessageType } from "./message"; const SENDER = "bitwarden-webauthn"; @@ -126,17 +124,11 @@ export class Messenger { } }; - let onDestroyListener; - const destroyPromise: Promise = new Promise((_, reject) => { - onDestroyListener = () => reject(new FallbackRequestedError()); - this.onDestroy.addEventListener("destroy", onDestroyListener); - }); + const onDestroyListener = () => abortController.abort(); + this.onDestroy.addEventListener("destroy", onDestroyListener); try { - const handlerResponse = await Promise.race([ - this.handler(message, abortController), - destroyPromise, - ]); + const handlerResponse = await this.handler(message, abortController); port.postMessage({ ...handlerResponse, SENDER }); } catch (error) { port.postMessage({ From b90ede079d3671ea2c1a2a54cd133f01bf889c74 Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Fri, 11 Apr 2025 22:55:02 -0400 Subject: [PATCH 09/19] [PM-18888] Fix duo redirect URL checks (#14174) * fix(PM-18888) : Create more strict checking of redirectURL to protect against open redirect attacks using regex. * fix : modify comments and check for embedded credentials. * feat : add testability to duo-redirect connector * fix : fixing strict typing; Removed styling from duo-redirect.ts which allows us to test without adding additional files and configurations for jest. * fix : remove duo-redirect.scss --- apps/web/src/connectors/duo-redirect.scss | 1 - apps/web/src/connectors/duo-redirect.spec.ts | 51 +++++++++++++ apps/web/src/connectors/duo-redirect.ts | 76 ++++++++++++++------ apps/web/webpack.config.js | 2 +- 4 files changed, 105 insertions(+), 25 deletions(-) delete mode 100644 apps/web/src/connectors/duo-redirect.scss create mode 100644 apps/web/src/connectors/duo-redirect.spec.ts diff --git a/apps/web/src/connectors/duo-redirect.scss b/apps/web/src/connectors/duo-redirect.scss deleted file mode 100644 index a4c7f9b25b7..00000000000 --- a/apps/web/src/connectors/duo-redirect.scss +++ /dev/null @@ -1 +0,0 @@ -@import "../scss/styles.scss"; diff --git a/apps/web/src/connectors/duo-redirect.spec.ts b/apps/web/src/connectors/duo-redirect.spec.ts new file mode 100644 index 00000000000..c0498861ba0 --- /dev/null +++ b/apps/web/src/connectors/duo-redirect.spec.ts @@ -0,0 +1,51 @@ +import { redirectToDuoFrameless } from "./duo-redirect"; + +describe("duo-redirect", () => { + describe("redirectToDuoFrameless", () => { + beforeEach(() => { + Object.defineProperty(window, "location", { + value: { href: "" }, + writable: true, + }); + }); + + it("should redirect to a valid Duo URL", () => { + const validUrl = "https://api-123.duosecurity.com/auth"; + redirectToDuoFrameless(validUrl); + expect(window.location.href).toBe(validUrl); + }); + + it("should redirect to a valid Duo Federal URL", () => { + const validUrl = "https://api-123.duofederal.com/auth"; + redirectToDuoFrameless(validUrl); + expect(window.location.href).toBe(validUrl); + }); + + it("should throw an error for an invalid URL", () => { + const invalidUrl = "https://malicious-site.com"; + expect(() => redirectToDuoFrameless(invalidUrl)).toThrow("Invalid redirect URL"); + }); + + it("should throw an error for an malicious URL with valid redirect embedded", () => { + const invalidUrl = "https://malicious-site.com\\@api-123.duosecurity.com/auth"; + expect(() => redirectToDuoFrameless(invalidUrl)).toThrow("Invalid redirect URL"); + }); + + it("should throw an error for a non-HTTPS URL", () => { + const nonHttpsUrl = "http://api-123.duosecurity.com/auth"; + expect(() => redirectToDuoFrameless(nonHttpsUrl)).toThrow("Invalid redirect URL"); + }); + + it("should throw an error for a URL with an invalid hostname", () => { + const invalidHostnameUrl = "https://api-123.invalid.com"; + expect(() => redirectToDuoFrameless(invalidHostnameUrl)).toThrow("Invalid redirect URL"); + }); + + it("should throw an error for a URL with credentials", () => { + const UrlWithCredentials = "https://api-123.duosecurity.com:password@evil/attack"; + expect(() => redirectToDuoFrameless(UrlWithCredentials)).toThrow( + "Invalid redirect URL: embedded credentials not allowed", + ); + }); + }); +}); diff --git a/apps/web/src/connectors/duo-redirect.ts b/apps/web/src/connectors/duo-redirect.ts index c19e056d306..d1841247962 100644 --- a/apps/web/src/connectors/duo-redirect.ts +++ b/apps/web/src/connectors/duo-redirect.ts @@ -1,14 +1,8 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { getQsParam } from "./common"; import { TranslationService } from "./translation.service"; -// FIXME: Remove when updating file. Eslint update -// eslint-disable-next-line @typescript-eslint/no-require-imports -require("./duo-redirect.scss"); - const mobileDesktopCallback = "bitwarden://duo-callback"; -let localeService: TranslationService = null; +let localeService: TranslationService | null = null; window.addEventListener("load", async () => { const redirectUrl = getQsParam("duoFramelessUrl"); @@ -18,9 +12,18 @@ window.addEventListener("load", async () => { return; } - const client = getQsParam("client"); - const code = getQsParam("code"); - const state = getQsParam("state"); + const client: string | null = getQsParam("client"); + const code: string | null = getQsParam("code"); + const state: string | null = getQsParam("state"); + if (!client) { + throw new Error("client is null"); + } + if (!code) { + throw new Error("code is null"); + } + if (!state) { + throw new Error("state is null"); + } localeService = new TranslationService(navigator.language, "locales"); await localeService.init(); @@ -53,16 +56,28 @@ window.addEventListener("load", async () => { * validate the Duo AuthUrl and redirect to it. * @param redirectUrl the duo auth url */ -function redirectToDuoFrameless(redirectUrl: string) { - const validateUrl = new URL(redirectUrl); - const validDuoUrl = - validateUrl.protocol === "https:" && - (validateUrl.hostname.endsWith(".duosecurity.com") || - validateUrl.hostname.endsWith(".duofederal.com")); - - if (!validDuoUrl) { +export function redirectToDuoFrameless(redirectUrl: string) { + // Regex to match a valid duo redirect URL + /** + * This regex checks for the following: + * The string must start with "https://api-" + * Followed by a subdomain that can contain letters, numbers + * Followed by either "duosecurity.com" or "duofederal.com" + * This ensures that the redirect does not contain any malicious content + * and is a valid Duo URL. + * */ + const duoRedirectUrlRegex = /^https:\/\/api-[a-zA-Z0-9]+\.(duosecurity|duofederal)\.com/; + // Check if the redirect URL matches the regex + if (!duoRedirectUrlRegex.test(redirectUrl)) { throw new Error("Invalid redirect URL"); } + // At this point we know the URL to be valid, but we need to check for embedded credentials + const validateUrl = new URL(redirectUrl); + // URLs should not contain + // Check that no embedded credentials are present + if (validateUrl.username || validateUrl.password) { + throw new Error("Invalid redirect URL: embedded credentials not allowed"); + } window.location.href = decodeURIComponent(redirectUrl); } @@ -72,17 +87,23 @@ function redirectToDuoFrameless(redirectUrl: string) { * so browser, desktop, and mobile are not able to take advantage of the countdown timer or close button. */ function displayHandoffMessage(client: string) { - const content = document.getElementById("content"); + const content: HTMLElement | null = document.getElementById("content"); + if (!content) { + throw new Error("content element not found"); + } content.className = "text-center"; content.innerHTML = ""; const h1 = document.createElement("h1"); - const p = document.createElement("p"); + const p: HTMLElement = document.createElement("p"); + if (!localeService) { + throw new Error("localeService is not initialized"); + } h1.textContent = localeService.t("youSuccessfullyLoggedIn"); p.textContent = client == "web" - ? (p.textContent = localeService.t("thisWindowWillCloseIn5Seconds")) + ? localeService.t("thisWindowWillCloseIn5Seconds") : localeService.t("youMayCloseThisWindow"); h1.className = "font-weight-semibold"; @@ -102,11 +123,20 @@ function displayHandoffMessage(client: string) { }); content.appendChild(button); - // Countdown timer (closes tab upon completion) - let num = Number(p.textContent.match(/\d+/)[0]); + if (p.textContent === null) { + throw new Error("count down container is null"); + } + const counterString: string | null = p.textContent.match(/\d+/)?.[0] || null; + if (!counterString) { + throw new Error("count down time cannot be null"); + } + let num: number = Number(counterString); const interval = setInterval(() => { if (num > 1) { + if (p.textContent === null) { + throw new Error("count down container is null"); + } p.textContent = p.textContent.replace(String(num), String(num - 1)); num--; } else { diff --git a/apps/web/webpack.config.js b/apps/web/webpack.config.js index d172ea95c71..9ccccee21bf 100644 --- a/apps/web/webpack.config.js +++ b/apps/web/webpack.config.js @@ -142,7 +142,7 @@ const plugins = [ new HtmlWebpackPlugin({ template: "./src/connectors/duo-redirect.html", filename: "duo-redirect-connector.html", - chunks: ["connectors/duo-redirect"], + chunks: ["connectors/duo-redirect", "styles"], }), new HtmlWebpackPlugin({ template: "./src/404.html", From 8b64087b32d9e44105e2d037e600514de46627ef Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Mon, 14 Apr 2025 14:41:08 +0200 Subject: [PATCH 10/19] [PM-18040] Inject ipc content script dynamically (#13674) * feat: add content script manager * feat: inject into all pages * feat: only inject if flag is enabled * fix: wrong constructor parameters --- .../browser/src/background/main.background.ts | 3 ++ .../ipc/ipc-content-script-manager.service.ts | 42 +++++++++++++++++++ libs/common/src/enums/feature-flag.enum.ts | 6 +++ 3 files changed, 51 insertions(+) create mode 100644 apps/browser/src/platform/ipc/ipc-content-script-manager.service.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 709d64f2094..a5001e0c5b7 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -261,6 +261,7 @@ import VaultTimeoutService from "../key-management/vault-timeout/vault-timeout.s import { BrowserApi } from "../platform/browser/browser-api"; import { flagEnabled } from "../platform/flags"; import { IpcBackgroundService } from "../platform/ipc/ipc-background.service"; +import { IpcContentScriptManagerService } from "../platform/ipc/ipc-content-script-manager.service"; import { UpdateBadge } from "../platform/listeners/update-badge"; /* eslint-disable no-restricted-imports */ import { ChromeMessageSender } from "../platform/messaging/chrome-message.sender"; @@ -405,6 +406,7 @@ export default class MainBackground { inlineMenuFieldQualificationService: InlineMenuFieldQualificationService; taskService: TaskService; + ipcContentScriptManagerService: IpcContentScriptManagerService; ipcService: IpcService; onUpdatedRan: boolean; @@ -1314,6 +1316,7 @@ export default class MainBackground { this.inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); + this.ipcContentScriptManagerService = new IpcContentScriptManagerService(this.configService); this.ipcService = new IpcBackgroundService(this.logService); } diff --git a/apps/browser/src/platform/ipc/ipc-content-script-manager.service.ts b/apps/browser/src/platform/ipc/ipc-content-script-manager.service.ts new file mode 100644 index 00000000000..e5fe95e2018 --- /dev/null +++ b/apps/browser/src/platform/ipc/ipc-content-script-manager.service.ts @@ -0,0 +1,42 @@ +import { mergeMap } from "rxjs"; + +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; + +import { BrowserApi } from "../browser/browser-api"; + +const IPC_CONTENT_SCRIPT_ID = "ipc-content-script"; + +export class IpcContentScriptManagerService { + constructor(configService: ConfigService) { + if (!BrowserApi.isManifestVersion(3)) { + // IPC not supported on MV2 + return; + } + + configService + .getFeatureFlag$(FeatureFlag.IpcChannelFramework) + .pipe( + mergeMap(async (enabled) => { + if (!enabled) { + return; + } + + try { + await BrowserApi.unregisterContentScriptsMv3({ ids: [IPC_CONTENT_SCRIPT_ID] }); + } catch { + // Ignore errors + } + + await BrowserApi.registerContentScriptsMv3([ + { + id: IPC_CONTENT_SCRIPT_ID, + matches: ["https://*/*"], + js: ["content/ipc-content-script.js"], + }, + ]); + }), + ) + .subscribe(); + } +} diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index cd88a415caf..09708859ac8 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -55,6 +55,9 @@ export enum FeatureFlag { VaultBulkManagementAction = "vault-bulk-management-action", SecurityTasks = "security-tasks", CipherKeyEncryption = "cipher-key-encryption", + + /* Platform */ + IpcChannelFramework = "ipc-channel-framework", } export type AllowedFeatureFlagTypes = boolean | number | string; @@ -118,6 +121,9 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.PrivateKeyRegeneration]: FALSE, [FeatureFlag.UserKeyRotationV2]: FALSE, [FeatureFlag.PM4154_BulkEncryptionService]: FALSE, + + /* Platform */ + [FeatureFlag.IpcChannelFramework]: FALSE, } satisfies Record; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue; From 5cc3ed7c5fba06c04b67e1870ea71322f21d72f5 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 14 Apr 2025 14:42:08 +0200 Subject: [PATCH 11/19] Move nodecryptofunctionservice codeownership (#14209) --- .github/CODEOWNERS | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6c7c43c4dac..2f402b15dd5 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -83,8 +83,6 @@ libs/common/src/platform @bitwarden/team-platform-dev libs/common/spec @bitwarden/team-platform-dev libs/common/src/state-migrations @bitwarden/team-platform-dev libs/platform @bitwarden/team-platform-dev -# Node-specifc platform files -libs/node @bitwarden/team-key-management-dev # Web utils used across app and connectors apps/web/src/utils/ @bitwarden/team-platform-dev # Web core and shared files @@ -146,6 +144,8 @@ apps/cli/src/key-management @bitwarden/team-key-management-dev libs/key-management @bitwarden/team-key-management-dev libs/key-management-ui @bitwarden/team-key-management-dev libs/common/src/key-management @bitwarden/team-key-management-dev +# Node-cryptofunction service +libs/node @bitwarden/team-key-management-dev apps/desktop/desktop_native/core/src/biometric/ @bitwarden/team-key-management-dev apps/desktop/src/services/native-messaging.service.ts @bitwarden/team-key-management-dev From 8885f5da24c7ecebf98c947625bc8545dc840135 Mon Sep 17 00:00:00 2001 From: Alexander Aronov Date: Mon, 14 Apr 2025 14:42:41 +0200 Subject: [PATCH 12/19] [PM-19914][PM-19913] trim domains and long fields in forwarders (#14141) * PM-19913: Added max length to the generated_for and description peroperties in the FirefoxRelay API payload * [PM-19913] Added maxLength restriction to the website and generatedBy methods. Added maxLength limit of 200 to the description of addy.io --- .../integration/integration-context.spec.ts | 37 +++++++++++++++++++ .../tools/integration/integration-context.ts | 26 ++++++++++--- .../core/src/integration/addy-io.spec.ts | 4 ++ .../generator/core/src/integration/addy-io.ts | 2 +- .../src/integration/firefox-relay.spec.ts | 5 +++ .../core/src/integration/firefox-relay.ts | 4 +- 6 files changed, 70 insertions(+), 8 deletions(-) diff --git a/libs/common/src/tools/integration/integration-context.spec.ts b/libs/common/src/tools/integration/integration-context.spec.ts index 67a40afb337..33694aefea1 100644 --- a/libs/common/src/tools/integration/integration-context.spec.ts +++ b/libs/common/src/tools/integration/integration-context.spec.ts @@ -189,6 +189,33 @@ describe("IntegrationContext", () => { expect(result).toBe(""); }); + + it("extracts the hostname when extractHostname is true", () => { + const context = new IntegrationContext(EXAMPLE_META, null, i18n); + + const result = context.website( + { website: "https://www.example.com/path" }, + { extractHostname: true }, + ); + + expect(result).toBe("www.example.com"); + }); + + it("falls back to the full URL when Utils.getHost cannot extract the hostname", () => { + const context = new IntegrationContext(EXAMPLE_META, null, i18n); + + const result = context.website({ website: "invalid-url" }, { extractHostname: true }); + + expect(result).toBe("invalid-url"); + }); + + it("truncates the website to maxLength", () => { + const context = new IntegrationContext(EXAMPLE_META, null, i18n); + + const result = context.website({ website: "www.example.com" }, { maxLength: 3 }); + + expect(result).toBe("www"); + }); }); describe("generatedBy", () => { @@ -211,5 +238,15 @@ describe("IntegrationContext", () => { expect(result).toBe("result"); expect(i18n.t).toHaveBeenCalledWith("forwarderGeneratedByWithWebsite", "www.example.com"); }); + + it("truncates generated text to maxLength", () => { + const context = new IntegrationContext(EXAMPLE_META, null, i18n); + i18n.t.mockReturnValue("This is the result text"); + + const result = context.generatedBy({ website: null }, { maxLength: 4 }); + + expect(result).toBe("This"); + expect(i18n.t).toHaveBeenCalledWith("forwarderGeneratedBy", ""); + }); }); }); diff --git a/libs/common/src/tools/integration/integration-context.ts b/libs/common/src/tools/integration/integration-context.ts index 40648df6803..49edafc026b 100644 --- a/libs/common/src/tools/integration/integration-context.ts +++ b/libs/common/src/tools/integration/integration-context.ts @@ -79,24 +79,40 @@ export class IntegrationContext { /** look up the website the integration is working with. * @param request supplies information about the state of the extension site + * @param options optional parameters + * @param options.extractHostname when `true`, tries to extract the hostname from the website URL, returns full URL otherwise + * @param options.maxLength limits the length of the return value * @returns The website or an empty string if a website isn't available * @remarks `website` is usually supplied when generating a credential from the vault */ - website(request: IntegrationRequest) { - return request.website ?? ""; + website( + request: IntegrationRequest, + options?: { extractHostname?: boolean; maxLength?: number }, + ) { + let url = request.website ?? ""; + if (options?.extractHostname) { + url = Utils.getHost(url) ?? url; + } + return url.slice(0, options?.maxLength); } /** look up localized text indicating Bitwarden requested the forwarding address. * @param request supplies information about the state of the extension site + * @param options optional parameters + * @param options.extractHostname when `true`, extracts the hostname from the website URL + * @param options.maxLength limits the length of the return value * @returns localized text describing a generated forwarding address */ - generatedBy(request: IntegrationRequest) { - const website = this.website(request); + generatedBy( + request: IntegrationRequest, + options?: { extractHostname?: boolean; maxLength?: number }, + ) { + const website = this.website(request, { extractHostname: options?.extractHostname ?? false }); const descriptionId = website === "" ? "forwarderGeneratedBy" : "forwarderGeneratedByWithWebsite"; const description = this.i18n.t(descriptionId, website); - return description; + return description.slice(0, options?.maxLength); } } diff --git a/libs/tools/generator/core/src/integration/addy-io.spec.ts b/libs/tools/generator/core/src/integration/addy-io.spec.ts index 9c816330616..40d17e9d888 100644 --- a/libs/tools/generator/core/src/integration/addy-io.spec.ts +++ b/libs/tools/generator/core/src/integration/addy-io.spec.ts @@ -55,6 +55,10 @@ describe("Addy.io forwarder", () => { const result = AddyIo.forwarder.createForwardingEmail.body(null, context); + expect(context.generatedBy).toHaveBeenCalledWith(null, { + extractHostname: true, + maxLength: 200, + }); expect(result).toEqual({ domain: "domain", description: "generated by", diff --git a/libs/tools/generator/core/src/integration/addy-io.ts b/libs/tools/generator/core/src/integration/addy-io.ts index 631c5fdb510..93ffed3392a 100644 --- a/libs/tools/generator/core/src/integration/addy-io.ts +++ b/libs/tools/generator/core/src/integration/addy-io.ts @@ -39,7 +39,7 @@ const createForwardingEmail = Object.freeze({ body(request: IntegrationRequest, context: ForwarderContext) { return { domain: context.emailDomain(), - description: context.generatedBy(request), + description: context.generatedBy(request, { extractHostname: true, maxLength: 200 }), }; }, hasJsonPayload(response: Response) { diff --git a/libs/tools/generator/core/src/integration/firefox-relay.spec.ts b/libs/tools/generator/core/src/integration/firefox-relay.spec.ts index ed487b7f49f..08798b154b3 100644 --- a/libs/tools/generator/core/src/integration/firefox-relay.spec.ts +++ b/libs/tools/generator/core/src/integration/firefox-relay.spec.ts @@ -56,6 +56,11 @@ describe("Firefox Relay forwarder", () => { const result = FirefoxRelay.forwarder.createForwardingEmail.body(null, context); + expect(context.website).toHaveBeenCalledWith(null, { maxLength: 255 }); + expect(context.generatedBy).toHaveBeenCalledWith(null, { + extractHostname: true, + maxLength: 64, + }); expect(result).toEqual({ enabled: true, generated_for: "website", diff --git a/libs/tools/generator/core/src/integration/firefox-relay.ts b/libs/tools/generator/core/src/integration/firefox-relay.ts index 9f40a3631ff..f80de0c95dd 100644 --- a/libs/tools/generator/core/src/integration/firefox-relay.ts +++ b/libs/tools/generator/core/src/integration/firefox-relay.ts @@ -33,8 +33,8 @@ const createForwardingEmail = Object.freeze({ body(request: IntegrationRequest, context: ForwarderContext) { return { enabled: true, - generated_for: context.website(request), - description: context.generatedBy(request), + generated_for: context.website(request, { maxLength: 255 }), + description: context.generatedBy(request, { extractHostname: true, maxLength: 64 }), }; }, hasJsonPayload(response: Response) { From f1a2acb0b9b69e9eb854f093362e7c462e31dbb0 Mon Sep 17 00:00:00 2001 From: Alec Rippberger <127791530+alec-livefront@users.noreply.github.com> Date: Mon, 14 Apr 2025 09:37:52 -0500 Subject: [PATCH 13/19] fix: [PM-20180] add OrganizationDuo to dialog title function Fix issue where modal was not displayed when clicking Manage option for 2FA on Organizations. This adds the OrganizationDuo case to the dialogTitle method to properly handle this provider type. PM-20180 --- .../app/auth/settings/two-factor/two-factor-verify.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-verify.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-verify.component.ts index 98cb7199d2a..a153a9ec56a 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-verify.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-verify.component.ts @@ -99,6 +99,7 @@ export class TwoFactorVerifyComponent { case -1 as TwoFactorProviderType: return this.i18nService.t("recoveryCodeTitle"); case TwoFactorProviderType.Duo: + case TwoFactorProviderType.OrganizationDuo: return "Duo"; case TwoFactorProviderType.Email: return this.i18nService.t("emailTitle"); From 7e621be6cb331ebce55c06a801dbdb06766da348 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Mon, 14 Apr 2025 10:46:58 -0500 Subject: [PATCH 14/19] [PM-18969] CSV importers should create nested collections (#14007) --- .../src/importers/base-importer.spec.ts | 164 ++++++++++++++++++ libs/importer/src/importers/base-importer.ts | 13 +- .../bitwarden/bitwarden-csv-importer.ts | 39 +---- .../keeper/keeper-csv-importer.spec.ts | 33 +++- ...etwrix-passwordsecure-csv-importer.spec.ts | 19 +- .../passwordxp-csv-importer.spec.ts | 23 ++- .../importers/roboform-csv-importer.spec.ts | 17 +- .../spec-data/keeper-csv/testdata.csv.ts | 6 + .../spec-data/netwrix-csv/login-export.csv.ts | 3 + .../passwordxp-with-folders.csv.ts | 14 ++ .../spec-data/roboform-csv/with-folders.ts | 7 + 11 files changed, 303 insertions(+), 35 deletions(-) diff --git a/libs/importer/src/importers/base-importer.spec.ts b/libs/importer/src/importers/base-importer.spec.ts index 309bb7ca8c4..4e3cdb355be 100644 --- a/libs/importer/src/importers/base-importer.spec.ts +++ b/libs/importer/src/importers/base-importer.spec.ts @@ -1,6 +1,9 @@ import { CipherType } from "@bitwarden/common/vault/enums"; import { CardView } from "@bitwarden/common/vault/models/view/card.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; + +import { ImportResult } from "../models"; import { BaseImporter } from "./base-importer"; @@ -16,8 +19,169 @@ class FakeBaseImporter extends BaseImporter { parseXml(data: string): Document { return super.parseXml(data); } + + processFolder(result: ImportResult, folderName: string, addRelationship: boolean = true): void { + return super.processFolder(result, folderName, addRelationship); + } } +describe("processFolder method", () => { + let result: ImportResult; + const importer = new FakeBaseImporter(); + + beforeEach(() => { + result = { + folders: [], + folderRelationships: [], + collections: [], + collectionRelationships: [], + ciphers: [], + success: false, + errorMessage: "", + }; + }); + + it("should add a new folder and relationship when folderName is unique", () => { + // arrange + // a folder exists - but it is not the same as the one we are importing + result = { + folders: [{ name: "ABC" } as FolderView], + folderRelationships: [], + collections: [], + collectionRelationships: [], + ciphers: [{ name: "cipher1", id: "cipher1" } as CipherView], + success: false, + errorMessage: "", + }; + importer.processFolder(result, "Folder1"); + + expect(result.folders).toHaveLength(2); + expect(result.folders[0].name).toBe("ABC"); + expect(result.folders[1].name).toBe("Folder1"); + expect(result.folderRelationships).toHaveLength(1); + expect(result.folderRelationships[0]).toEqual([1, 1]); // cipher1 -> Folder1 + }); + + it("should not add duplicate folders and should add relationships", () => { + // setup + // folder called "Folder1" already exists + result = { + folders: [{ name: "Folder1" } as FolderView], + folderRelationships: [], + collections: [], + collectionRelationships: [], + ciphers: [{ name: "cipher1", id: "cipher1" } as CipherView], + success: false, + errorMessage: "", + }; + + // import an existing folder should not add to the result.folders + importer.processFolder(result, "Folder1"); + + expect(result.folders).toHaveLength(1); + expect(result.folders[0].name).toBe("Folder1"); + expect(result.folderRelationships).toHaveLength(1); + expect(result.folderRelationships[0]).toEqual([1, 0]); // cipher1 -> folder1 + }); + + it("should create parent folders for nested folder names but not duplicates", () => { + // arrange + result = { + folders: [ + { name: "Ancestor/Parent/Child" } as FolderView, + { name: "Ancestor" } as FolderView, + ], + folderRelationships: [], + collections: [], + collectionRelationships: [], + ciphers: [{ name: "cipher1", id: "cipher1" } as CipherView], + success: false, + errorMessage: "", + }; + + // act + // importing an existing folder with a relationship should not change the result.folders + // nor should it change the result.folderRelationships + importer.processFolder(result, "Ancestor/Parent/Child/Grandchild/GreatGrandchild"); + + expect(result.folders).toHaveLength(5); + expect(result.folders.map((f) => f.name)).toEqual([ + "Ancestor/Parent/Child", + "Ancestor", + "Ancestor/Parent/Child/Grandchild/GreatGrandchild", + "Ancestor/Parent/Child/Grandchild", + "Ancestor/Parent", + ]); + expect(result.folderRelationships).toHaveLength(1); + expect(result.folderRelationships[0]).toEqual([1, 2]); // cipher1 -> grandchild + }); + + it("should not affect existing relationships", () => { + // arrange + // "Parent" is a folder with no relationship + // "Child" is a folder with 2 ciphers + result = { + folders: [{ name: "Parent" } as FolderView, { name: "Parent/Child" } as FolderView], + folderRelationships: [ + [1, 1], + [2, 1], + ], + collections: [], + collectionRelationships: [], + ciphers: [ + { name: "cipher1", id: "cipher1" } as CipherView, + { name: "cipher2", id: "cipher2" } as CipherView, + { name: "cipher3", id: "cipher3" } as CipherView, + ], + success: false, + errorMessage: "", + }; + + // act + // importing an existing folder with a relationship should not change the result.folders + // nor should it change the result.folderRelationships + importer.processFolder(result, "Parent/Child/Grandchild"); + + expect(result.folders).toHaveLength(3); + expect(result.folders.map((f) => f.name)).toEqual([ + "Parent", + "Parent/Child", + "Parent/Child/Grandchild", + ]); + expect(result.folderRelationships).toHaveLength(3); + expect(result.folderRelationships[0]).toEqual([1, 1]); // cipher1 -> child + expect(result.folderRelationships[1]).toEqual([2, 1]); // cipher2 -> child + expect(result.folderRelationships[2]).toEqual([3, 2]); // cipher3 -> grandchild + }); + + it("should not add relationships if addRelationship is false", () => { + importer.processFolder(result, "Folder1", false); + + expect(result.folders).toHaveLength(1); + expect(result.folders[0].name).toBe("Folder1"); + expect(result.folderRelationships).toHaveLength(0); + }); + + it("should replace backslashes with forward slashes in folder names", () => { + importer.processFolder(result, "Parent\\Child\\Grandchild"); + + expect(result.folders).toHaveLength(3); + expect(result.folders.map((f) => f.name)).toEqual([ + "Parent/Child/Grandchild", + "Parent/Child", + "Parent", + ]); + }); + + it("should handle empty or null folder names gracefully", () => { + importer.processFolder(result, null); + importer.processFolder(result, ""); + + expect(result.folders).toHaveLength(0); + expect(result.folderRelationships).toHaveLength(0); + }); +}); + describe("BaseImporter class", () => { const importer = new FakeBaseImporter(); let cipher: CipherView; diff --git a/libs/importer/src/importers/base-importer.ts b/libs/importer/src/importers/base-importer.ts index 90af5344cfc..0594b6014e8 100644 --- a/libs/importer/src/importers/base-importer.ts +++ b/libs/importer/src/importers/base-importer.ts @@ -366,7 +366,7 @@ export abstract class BaseImporter { let folderIndex = result.folders.length; // Replace backslashes with forward slashes, ensuring we create sub-folders - folderName = folderName.replace("\\", "/"); + folderName = folderName.replace(/\\/g, "/"); let addFolder = true; for (let i = 0; i < result.folders.length; i++) { @@ -387,6 +387,17 @@ export abstract class BaseImporter { if (addRelationship) { result.folderRelationships.push([result.ciphers.length, folderIndex]); } + + // if the folder name is a/b/c/d, we need to create a/b/c and a/b and a + const parts = folderName.split("/"); + for (let i = parts.length - 1; i > 0; i--) { + const parentName = parts.slice(0, i).join("/") as string; + if (result.folders.find((c) => c.name === parentName) == null) { + const folder = new FolderView(); + folder.name = parentName; + result.folders.push(folder); + } + } } protected convertToNoteIfNeeded(cipher: CipherView) { diff --git a/libs/importer/src/importers/bitwarden/bitwarden-csv-importer.ts b/libs/importer/src/importers/bitwarden/bitwarden-csv-importer.ts index fab47b30b1a..abda9a04a8a 100644 --- a/libs/importer/src/importers/bitwarden/bitwarden-csv-importer.ts +++ b/libs/importer/src/importers/bitwarden/bitwarden-csv-importer.ts @@ -1,6 +1,5 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { CollectionView } from "@bitwarden/admin-console/common"; import { FieldType, SecureNoteType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -25,35 +24,11 @@ export class BitwardenCsvImporter extends BaseImporter implements Importer { if (this.organization && !this.isNullOrWhitespace(value.collections)) { const collections = (value.collections as string).split(","); collections.forEach((col) => { - let addCollection = true; - let collectionIndex = result.collections.length; - - for (let i = 0; i < result.collections.length; i++) { - if (result.collections[i].name === col) { - addCollection = false; - collectionIndex = i; - break; - } - } - - if (addCollection) { - const collection = new CollectionView(); - collection.name = col; - result.collections.push(collection); - } - - result.collectionRelationships.push([result.ciphers.length, collectionIndex]); - - // if the collection name is a/b/c/d, we need to create a/b/c and a/b and a - const parts = col.split("/"); - for (let i = parts.length - 1; i > 0; i--) { - const parentCollectionName = parts.slice(0, i).join("/") as string; - if (result.collections.find((c) => c.name === parentCollectionName) == null) { - const parentCollection = new CollectionView(); - parentCollection.name = parentCollectionName; - result.collections.push(parentCollection); - } - } + // here processFolder is used to create collections + // In an Organization folders are converted to collections + // see line just before this function terminates + // where all folders are turned to collections + this.processFolder(result, col); }); } else if (!this.organization) { this.processFolder(result, value.folder); @@ -125,6 +100,10 @@ export class BitwardenCsvImporter extends BaseImporter implements Importer { result.ciphers.push(cipher); }); + if (this.organization) { + this.moveFoldersToCollections(result); + } + result.success = true; return Promise.resolve(result); } diff --git a/libs/importer/src/importers/keeper/keeper-csv-importer.spec.ts b/libs/importer/src/importers/keeper/keeper-csv-importer.spec.ts index 69655eb9177..d7a4d487bcb 100644 --- a/libs/importer/src/importers/keeper/keeper-csv-importer.spec.ts +++ b/libs/importer/src/importers/keeper/keeper-csv-importer.spec.ts @@ -1,6 +1,9 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { testData as TestData } from "../spec-data/keeper-csv/testdata.csv"; +import { + testData as TestData, + testDataMultiCollection, +} from "../spec-data/keeper-csv/testdata.csv"; import { KeeperCsvImporter } from "./keeper-csv-importer"; @@ -121,4 +124,32 @@ describe("Keeper CSV Importer", () => { expect(result.collectionRelationships[1]).toEqual([1, 0]); expect(result.collectionRelationships[2]).toEqual([2, 1]); }); + + it("should create collections tree, with child collections and relationships", async () => { + importer.organizationId = Utils.newGuid(); + const result = await importer.parse(testDataMultiCollection); + expect(result != null).toBe(true); + + const collections = result.collections; + expect(collections).not.toBeNull(); + expect(collections.length).toBe(3); + + // collection with the cipher + const collections1 = collections.shift(); + expect(collections1.name).toBe("Foo/Baz/Bar"); + + //second level collection + const collections2 = collections.shift(); + expect(collections2.name).toBe("Foo/Baz"); + + //third level + const collections3 = collections.shift(); + expect(collections3.name).toBe("Foo"); + + // [Cipher, Folder] + expect(result.collectionRelationships.length).toBe(3); + expect(result.collectionRelationships[0]).toEqual([0, 0]); + expect(result.collectionRelationships[1]).toEqual([1, 1]); + expect(result.collectionRelationships[2]).toEqual([2, 2]); + }); }); diff --git a/libs/importer/src/importers/netwrix/netwrix-passwordsecure-csv-importer.spec.ts b/libs/importer/src/importers/netwrix/netwrix-passwordsecure-csv-importer.spec.ts index ff327daf04d..8736b3df0c8 100644 --- a/libs/importer/src/importers/netwrix/netwrix-passwordsecure-csv-importer.spec.ts +++ b/libs/importer/src/importers/netwrix/netwrix-passwordsecure-csv-importer.spec.ts @@ -1,6 +1,9 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { credentialsData } from "../spec-data/netwrix-csv/login-export.csv"; +import { + credentialsData, + credentialsDataWithFolders, +} from "../spec-data/netwrix-csv/login-export.csv"; import { NetwrixPasswordSecureCsvImporter } from "./netwrix-passwordsecure-csv-importer"; @@ -88,4 +91,18 @@ describe("Netwrix Password Secure CSV Importer", () => { expect(result.collectionRelationships[1]).toEqual([1, 1]); expect(result.collectionRelationships[2]).toEqual([2, 0]); }); + + it("should parse multiple collections", async () => { + importer.organizationId = Utils.newGuid(); + const result = await importer.parse(credentialsDataWithFolders); + + expect(result).not.toBeNull(); + expect(result.success).toBe(true); + expect(result.collections.length).toBe(3); + expect(result.collections[0].name).toBe("folder1/folder2/folder3"); + expect(result.collections[1].name).toBe("folder1/folder2"); + expect(result.collections[2].name).toBe("folder1"); + expect(result.collectionRelationships.length).toBe(1); + expect(result.collectionRelationships[0]).toEqual([0, 0]); + }); }); diff --git a/libs/importer/src/importers/passsordxp/passwordxp-csv-importer.spec.ts b/libs/importer/src/importers/passsordxp/passwordxp-csv-importer.spec.ts index 0decd1e2830..12cfbbe62bb 100644 --- a/libs/importer/src/importers/passsordxp/passwordxp-csv-importer.spec.ts +++ b/libs/importer/src/importers/passsordxp/passwordxp-csv-importer.spec.ts @@ -4,7 +4,10 @@ import { ImportResult } from "../../models/import-result"; import { dutchHeaders } from "../spec-data/passwordxp-csv/dutch-headers"; import { germanHeaders } from "../spec-data/passwordxp-csv/german-headers"; import { noFolder } from "../spec-data/passwordxp-csv/no-folder.csv"; -import { withFolders } from "../spec-data/passwordxp-csv/passwordxp-with-folders.csv"; +import { + withFolders, + withMultipleFolders, +} from "../spec-data/passwordxp-csv/passwordxp-with-folders.csv"; import { withoutFolders } from "../spec-data/passwordxp-csv/passwordxp-without-folders.csv"; import { PasswordXPCsvImporter } from "./passwordxp-csv-importer"; @@ -167,4 +170,22 @@ describe("PasswordXPCsvImporter", () => { expect(collectionRelationship).toEqual([4, 2]); collectionRelationship = result.collectionRelationships.shift(); }); + + it("should convert multi-level folders to collections when importing into an organization", async () => { + importer.organizationId = "someOrg"; + const result: ImportResult = await importer.parse(withMultipleFolders); + expect(result.success).toBe(true); + expect(result.ciphers.length).toBe(5); + + expect(result.collections.length).toBe(3); + expect(result.collections[0].name).toEqual("Test Folder"); + expect(result.collections[1].name).toEqual("Test Folder/Level 2 Folder"); + expect(result.collections[2].name).toEqual("Test Folder/Level 2 Folder/Level 3 Folder"); + + expect(result.collectionRelationships.length).toBe(4); + expect(result.collectionRelationships[0]).toEqual([1, 0]); + expect(result.collectionRelationships[1]).toEqual([2, 1]); + expect(result.collectionRelationships[2]).toEqual([3, 1]); + expect(result.collectionRelationships[3]).toEqual([4, 2]); + }); }); diff --git a/libs/importer/src/importers/roboform-csv-importer.spec.ts b/libs/importer/src/importers/roboform-csv-importer.spec.ts index dd385e10b8d..23604042a02 100644 --- a/libs/importer/src/importers/roboform-csv-importer.spec.ts +++ b/libs/importer/src/importers/roboform-csv-importer.spec.ts @@ -2,7 +2,7 @@ import { CipherType } from "@bitwarden/common/vault/enums"; import { RoboFormCsvImporter } from "./roboform-csv-importer"; import { data as dataNoFolder } from "./spec-data/roboform-csv/empty-folders"; -import { data as dataFolder } from "./spec-data/roboform-csv/with-folders"; +import { data as dataFolder, dataWithFolderHierarchy } from "./spec-data/roboform-csv/with-folders"; describe("Roboform CSV Importer", () => { beforeEach(() => { @@ -39,4 +39,19 @@ describe("Roboform CSV Importer", () => { expect(result.ciphers[4].notes).toBe("This is a safe note"); expect(result.ciphers[4].name).toBe("note - 2023-03-31"); }); + + it("should parse CSV data with folder hierarchy", async () => { + const importer = new RoboFormCsvImporter(); + const result = await importer.parse(dataWithFolderHierarchy); + expect(result != null).toBe(true); + + expect(result.folders.length).toBe(5); + expect(result.ciphers.length).toBe(5); + + expect(result.folders[0].name).toBe("folder1"); + expect(result.folders[1].name).toBe("folder2"); + expect(result.folders[2].name).toBe("folder2/folder3"); + expect(result.folders[3].name).toBe("folder1/folder2/folder3"); + expect(result.folders[4].name).toBe("folder1/folder2"); + }); }); diff --git a/libs/importer/src/importers/spec-data/keeper-csv/testdata.csv.ts b/libs/importer/src/importers/spec-data/keeper-csv/testdata.csv.ts index a40e97ff3fd..cfa51faecc6 100644 --- a/libs/importer/src/importers/spec-data/keeper-csv/testdata.csv.ts +++ b/libs/importer/src/importers/spec-data/keeper-csv/testdata.csv.ts @@ -2,3 +2,9 @@ export const testData = `"Foo","Bar","john.doe@example.com","1234567890abcdef"," "Foo","Bar 1","john.doe1@example.com","234567890abcdef1","https://an.example.com/","","","Account ID","12345","Org ID","54321" "Foo\\Baz","Bar 2","john.doe2@example.com","34567890abcdef12","https://another.example.com/","","","Account ID","23456","TFC:Keeper","otpauth://totp/Amazon:me@company.com?secret=JBSWY3DPEHPK3PXP&issuer=Amazon&algorithm=SHA1&digits=6&period=30" `; + +export const testDataMultiCollection = ` +"Foo\\Baz\\Bar","Bar 2","john.doe2@example.com","34567890abcdef12","https://another.example.com/","","","Account ID","23456","TFC:Keeper","otpauth://totp/Amazon:me@company.com?secret=JBSWY3DPEHPK3PXP&issuer=Amazon&algorithm=SHA1&digits=6&period=30" +"Foo\\Baz","Bar 2","john.doe2@example.com","34567890abcdef12","https://another.example.com/","","","Account ID","23456","TFC:Keeper","otpauth://totp/Amazon:me@company.com?secret=JBSWY3DPEHPK3PXP&issuer=Amazon&algorithm=SHA1&digits=6&period=30" +"Foo","Bar 2","john.doe2@example.com","34567890abcdef12","https://another.example.com/","","","Account ID","23456","TFC:Keeper","otpauth://totp/Amazon:me@company.com?secret=JBSWY3DPEHPK3PXP&issuer=Amazon&algorithm=SHA1&digits=6&period=30" +`; diff --git a/libs/importer/src/importers/spec-data/netwrix-csv/login-export.csv.ts b/libs/importer/src/importers/spec-data/netwrix-csv/login-export.csv.ts index 715dd8e0074..5b0fa0c5cb2 100644 --- a/libs/importer/src/importers/spec-data/netwrix-csv/login-export.csv.ts +++ b/libs/importer/src/importers/spec-data/netwrix-csv/login-export.csv.ts @@ -2,3 +2,6 @@ "folderOrCollection1";"tag1, tag2, tag3";"Test Entry 1";"someUser";"somePassword";"https://www.example.com";"some note for example.com";"someTOTPSeed" "folderOrCollection2";"tag2";"Test Entry 2";"jdoe";"})9+Kg2fz_O#W1§H1-0Zio";"www.123.com";"Description123";"anotherTOTP" "folderOrCollection1";"someTag";"Test Entry 3";"username";"password";"www.internetsite.com";"Information";""`; + +export const credentialsDataWithFolders = `"Organisationseinheit";"DataTags";"Beschreibung";"Benutzername";"Passwort";"Internetseite";"Informationen";"One-Time Passwort" +"folder1\\folder2\\folder3";"tag1, tag2, tag3";"Test Entry 1";"someUser";"somePassword";"https://www.example.com";"some note for example.com";"someTOTPSeed"`; diff --git a/libs/importer/src/importers/spec-data/passwordxp-csv/passwordxp-with-folders.csv.ts b/libs/importer/src/importers/spec-data/passwordxp-csv/passwordxp-with-folders.csv.ts index c7cfe825759..884929cfc8f 100644 --- a/libs/importer/src/importers/spec-data/passwordxp-csv/passwordxp-with-folders.csv.ts +++ b/libs/importer/src/importers/spec-data/passwordxp-csv/passwordxp-with-folders.csv.ts @@ -11,3 +11,17 @@ test;testtest;;http://test;test;27-3-2024 12:36:59;27-3-2024 12:36:59;;; [Cert folder\\Nested folder]; test2;testtest;;http://test;test;27-3-2024 12:36:59;27-3-2024 12:36:59;;;`; + +export const withMultipleFolders = `Title;User name;Account;URL;Password;Modified;Created;Expire on;Description;Modified by +>>> +Title2;Username2;Account2;http://URL2.com;12345678;27-3-2024 08:11:21;27-3-2024 08:11:21;;; + +[Test Folder] +Title Test 1;Username1;Account1;http://URL1.com;Password1;27-3-2024 08:10:52;27-3-2024 08:10:52;;; + +[Test Folder\\Level 2 Folder] +Certificate 1;;;;;27-3-2024 10:22:39;27-3-2024 10:22:39;;; +test;testtest;;http://test;test;27-3-2024 12:36:59;27-3-2024 12:36:59;;; + +[Test Folder\\Level 2 Folder\\Level 3 Folder] +test2;testtest;;http://test;test;27-3-2024 12:36:59;27-3-2024 12:36:59;;;`; diff --git a/libs/importer/src/importers/spec-data/roboform-csv/with-folders.ts b/libs/importer/src/importers/spec-data/roboform-csv/with-folders.ts index e836c6430f0..86757b79c86 100644 --- a/libs/importer/src/importers/spec-data/roboform-csv/with-folders.ts +++ b/libs/importer/src/importers/spec-data/roboform-csv/with-folders.ts @@ -4,3 +4,10 @@ Test,https://www.test.com/,https://www.test.com/,test@gmail.com,:testPassword,te LoginWebsite,https://login.Website.com/,https://login.Website.com/,test@outlook.com,123password,,folder2,"User ID$,,,txt,test@outlook.com","Password$,,,pwd,123password" Website,https://signin.website.com/,https://signin.website.com/,user@bitwarden.com,password123,Website ,folder3,"User ID$,,,txt,user@bitwarden.com","Password$,,,pwd,password123" note - 2023-03-31,,,,,This is a safe note,`; + +export const dataWithFolderHierarchy = `Name,Url,MatchUrl,Login,Pwd,Note,Folder,RfFieldsV2 +Bitwarden,https://bitwarden.com,https://bitwarden.com,user@bitwarden.com,password,,folder1,"User ID$,,,txt,user@bitwarden.com","Password$,,,pwd,password" +Test,https://www.test.com/,https://www.test.com/,test@gmail.com,:testPassword,test,folder1,"User ID$,,,txt,test@gmail.com","Password$,,,pwd,:testPassword" +LoginWebsite,https://login.Website.com/,https://login.Website.com/,test@outlook.com,123password,,folder2,"User ID$,,,txt,test@outlook.com","Password$,,,pwd,123password" +Website,https://signin.website.com/,https://signin.website.com/,user@bitwarden.com,password123,Website ,folder2\\folder3,"User ID$,,,txt,user@bitwarden.com","Password$,,,pwd,password123" +Website,https://signin.website.com/,https://signin.website.com/,user@bitwarden.com,password123,Website ,folder1\\folder2\\folder3,"User ID$,,,txt,user@bitwarden.com","Password$,,,pwd,password123"`; From 5b43be780b9085ed3e4094de22d727f7fb97f720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lison=20Fernandes?= Date: Mon, 14 Apr 2025 16:56:09 +0100 Subject: [PATCH 15/19] Remove duplicated copy (#14271) --- .github/DISCUSSION_TEMPLATE/password-manager.yml | 2 +- .github/DISCUSSION_TEMPLATE/secrets-manager.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/DISCUSSION_TEMPLATE/password-manager.yml b/.github/DISCUSSION_TEMPLATE/password-manager.yml index bc3938c1962..1d464ca9504 100644 --- a/.github/DISCUSSION_TEMPLATE/password-manager.yml +++ b/.github/DISCUSSION_TEMPLATE/password-manager.yml @@ -3,7 +3,7 @@ body: - type: markdown attributes: value: | - If you would like to contribute code to the Bitwarden codebase for consideration, please review [https://contributing.bitwarden.com/](https://contributing.bitwarden.com/) before posting. To keep discussion on topic, posts that do not include a proposal for a code contribution you wish to develop will be removed. For feature requests and community discussion, please visit https://community.bitwarden.com/ + If you would like to contribute code to the Bitwarden codebase for consideration, please review [https://contributing.bitwarden.com/](https://contributing.bitwarden.com/) before posting. To keep discussion on topic, posts that do not include a proposal for a code contribution you wish to develop will be removed. - type: dropdown attributes: label: Select Topic Area diff --git a/.github/DISCUSSION_TEMPLATE/secrets-manager.yml b/.github/DISCUSSION_TEMPLATE/secrets-manager.yml index bc3938c1962..1d464ca9504 100644 --- a/.github/DISCUSSION_TEMPLATE/secrets-manager.yml +++ b/.github/DISCUSSION_TEMPLATE/secrets-manager.yml @@ -3,7 +3,7 @@ body: - type: markdown attributes: value: | - If you would like to contribute code to the Bitwarden codebase for consideration, please review [https://contributing.bitwarden.com/](https://contributing.bitwarden.com/) before posting. To keep discussion on topic, posts that do not include a proposal for a code contribution you wish to develop will be removed. For feature requests and community discussion, please visit https://community.bitwarden.com/ + If you would like to contribute code to the Bitwarden codebase for consideration, please review [https://contributing.bitwarden.com/](https://contributing.bitwarden.com/) before posting. To keep discussion on topic, posts that do not include a proposal for a code contribution you wish to develop will be removed. - type: dropdown attributes: label: Select Topic Area From ac1210a7ed1ead4c786cac19e172367b1ae9b704 Mon Sep 17 00:00:00 2001 From: Bryan Cunningham Date: Mon, 14 Apr 2025 12:56:30 -0400 Subject: [PATCH 16/19] remove margin from checkbox hint (#14251) --- libs/components/src/form-control/form-control.component.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libs/components/src/form-control/form-control.component.html b/libs/components/src/form-control/form-control.component.html index b15202b0223..cc9c3dabbb6 100644 --- a/libs/components/src/form-control/form-control.component.html +++ b/libs/components/src/form-control/form-control.component.html @@ -14,7 +14,9 @@ } @if (!hasError) { - + + + } From 95ea1b22aea9389ee2853237d82f761268a2928a Mon Sep 17 00:00:00 2001 From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Date: Mon, 14 Apr 2025 12:47:09 -0500 Subject: [PATCH 17/19] [PM-17987] Add feature flag (#13991) * Add feature flag * Add unit tests. --- libs/common/src/enums/feature-flag.enum.ts | 2 + .../encrypt.service.implementation.ts | 21 +++++- .../crypto/services/encrypt.service.spec.ts | 70 ++++++++++++++++++- 3 files changed, 90 insertions(+), 3 deletions(-) diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 09708859ac8..fa776285ead 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -39,6 +39,7 @@ export enum FeatureFlag { PrivateKeyRegeneration = "pm-12241-private-key-regeneration", UserKeyRotationV2 = "userkey-rotation-v2", PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service", + PM17987_BlockType0 = "pm-17987-block-type-0", /* Tools */ ItemShare = "item-share", @@ -121,6 +122,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.PrivateKeyRegeneration]: FALSE, [FeatureFlag.UserKeyRotationV2]: FALSE, [FeatureFlag.PM4154_BulkEncryptionService]: FALSE, + [FeatureFlag.PM17987_BlockType0]: FALSE, /* Platform */ [FeatureFlag.IpcChannelFramework]: FALSE, diff --git a/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts index d10061a2be8..10d29198ada 100644 --- a/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts @@ -19,10 +19,17 @@ import { SymmetricCryptoKey, } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { + DefaultFeatureFlagValue, + FeatureFlag, + getFeatureFlagValue, +} from "../../../enums/feature-flag.enum"; import { ServerConfig } from "../../../platform/abstractions/config/server-config"; import { EncryptService } from "../abstractions/encrypt.service"; export class EncryptServiceImplementation implements EncryptService { + private blockType0: boolean = DefaultFeatureFlagValue[FeatureFlag.PM17987_BlockType0]; + constructor( protected cryptoFunctionService: CryptoFunctionService, protected logService: LogService, @@ -31,7 +38,7 @@ export class EncryptServiceImplementation implements EncryptService { // Handle updating private properties to turn on/off feature flags. onServerConfigChange(newConfig: ServerConfig): void { - return; + this.blockType0 = getFeatureFlagValue(newConfig, FeatureFlag.PM17987_BlockType0); } async encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise { @@ -39,6 +46,12 @@ export class EncryptServiceImplementation implements EncryptService { throw new Error("No encryption key provided."); } + if (this.blockType0) { + if (key.encType === EncryptionType.AesCbc256_B64 || key.key.byteLength < 64) { + throw new Error("Type 0 encryption is not supported."); + } + } + if (plainValue == null) { return Promise.resolve(null); } @@ -70,6 +83,12 @@ export class EncryptServiceImplementation implements EncryptService { throw new Error("No encryption key provided."); } + if (this.blockType0) { + if (key.encType === EncryptionType.AesCbc256_B64 || key.key.byteLength < 64) { + throw new Error("Type 0 encryption is not supported."); + } + } + const innerKey = key.inner(); if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) { const encValue = await this.aesEncrypt(plainValue, innerKey); diff --git a/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts b/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts index 275fd266f84..c65c78d88d7 100644 --- a/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts @@ -10,6 +10,8 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym import { CsprngArray } from "@bitwarden/common/types/csprng"; import { makeStaticByteArray } from "../../../../spec"; +import { DefaultFeatureFlagValue, FeatureFlag } from "../../../enums/feature-flag.enum"; +import { ServerConfig } from "../../../platform/abstractions/config/server-config"; import { EncryptServiceImplementation } from "./encrypt.service.implementation"; @@ -26,17 +28,65 @@ describe("EncryptService", () => { encryptService = new EncryptServiceImplementation(cryptoFunctionService, logService, true); }); + describe("onServerConfigChange", () => { + const newConfig = mock(); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it("updates internal flag with default value when not present in config", () => { + encryptService.onServerConfigChange(newConfig); + + expect((encryptService as any).blockType0).toBe( + DefaultFeatureFlagValue[FeatureFlag.PM17987_BlockType0], + ); + }); + + test.each([true, false])("updates internal flag with value in config", (expectedValue) => { + newConfig.featureStates = { [FeatureFlag.PM17987_BlockType0]: expectedValue }; + + encryptService.onServerConfigChange(newConfig); + + expect((encryptService as any).blockType0).toBe(expectedValue); + }); + }); + describe("encrypt", () => { it("throws if no key is provided", () => { return expect(encryptService.encrypt(null, null)).rejects.toThrow( "No encryption key provided.", ); }); - it("returns null if no data is provided", async () => { - const key = mock(); + + it("throws if type 0 key is provided with flag turned on", async () => { + (encryptService as any).blockType0 = true; + const key = new SymmetricCryptoKey(makeStaticByteArray(32)); + const mock32Key = mock(); + mock32Key.key = makeStaticByteArray(32); + + await expect(encryptService.encrypt(null!, key)).rejects.toThrow( + "Type 0 encryption is not supported.", + ); + await expect(encryptService.encrypt(null!, mock32Key)).rejects.toThrow( + "Type 0 encryption is not supported.", + ); + + const plainValue = "data"; + await expect(encryptService.encrypt(plainValue, key)).rejects.toThrow( + "Type 0 encryption is not supported.", + ); + await expect(encryptService.encrypt(plainValue, mock32Key)).rejects.toThrow( + "Type 0 encryption is not supported.", + ); + }); + + it("returns null if no data is provided with valid key", async () => { + const key = new SymmetricCryptoKey(makeStaticByteArray(64)); const actual = await encryptService.encrypt(null, key); expect(actual).toBeNull(); }); + it("creates an EncString for Aes256Cbc", async () => { const key = new SymmetricCryptoKey(makeStaticByteArray(32)); const plainValue = "data"; @@ -53,6 +103,7 @@ describe("EncryptService", () => { expect(Utils.fromB64ToArray(result.data).length).toEqual(4); expect(Utils.fromB64ToArray(result.iv).length).toEqual(16); }); + it("creates an EncString for Aes256Cbc_HmacSha256_B64", async () => { const key = new SymmetricCryptoKey(makeStaticByteArray(64)); const plainValue = "data"; @@ -90,6 +141,21 @@ describe("EncryptService", () => { ); }); + it("throws if type 0 key provided with flag turned on", async () => { + (encryptService as any).blockType0 = true; + const key = new SymmetricCryptoKey(makeStaticByteArray(32)); + const mock32Key = mock(); + mock32Key.key = makeStaticByteArray(32); + + await expect(encryptService.encryptToBytes(plainValue, key)).rejects.toThrow( + "Type 0 encryption is not supported.", + ); + + await expect(encryptService.encryptToBytes(plainValue, mock32Key)).rejects.toThrow( + "Type 0 encryption is not supported.", + ); + }); + it("encrypts data with provided Aes256Cbc key and returns correct encbuffer", async () => { const key = new SymmetricCryptoKey(makeStaticByteArray(32, 0)); const iv = makeStaticByteArray(16, 80); From 356a20a4bce174cecf98ee6545b569bc2439414e Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Mon, 14 Apr 2025 15:55:01 -0400 Subject: [PATCH 18/19] fix(login): [PM-20174] Do not show validation errors on email input on LoginComponent * Do not show validation errors on input * Removed one-line function. * Removed awaits --- .../auth/src/angular/login/login.component.ts | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts index 55c282be55c..8a198663e06 100644 --- a/libs/auth/src/angular/login/login.component.ts +++ b/libs/auth/src/angular/login/login.component.ts @@ -400,12 +400,6 @@ export class LoginComponent implements OnInit, OnDestroy { await this.router.navigate(["/login-with-device"]); } - protected async emailIsValid(): Promise { - this.formGroup.controls.email.markAsTouched(); - this.formGroup.controls.email.updateValueAndValidity({ onlySelf: true, emitEvent: true }); - return this.formGroup.controls.email.valid; - } - protected async toggleLoginUiState(value: LoginUiState): Promise { this.loginUiState = value; @@ -474,7 +468,7 @@ export class LoginComponent implements OnInit, OnDestroy { * Continue to the master password entry state (only if email is validated) */ protected async continue(): Promise { - const isEmailValid = await this.emailIsValid(); + const isEmailValid = this.validateEmail(); if (isEmailValid) { await this.toggleLoginUiState(LoginUiState.MASTER_PASSWORD_ENTRY); @@ -496,7 +490,7 @@ export class LoginComponent implements OnInit, OnDestroy { */ async handleSsoClick() { // Make sure the email is valid - const isEmailValid = await this.emailIsValid(); + const isEmailValid = this.validateEmail(); if (!isEmailValid) { return; } @@ -594,11 +588,21 @@ export class LoginComponent implements OnInit, OnDestroy { } }; + /** + * Validates the email and displays any validation errors. + * @returns true if the email is valid, false otherwise. + */ + protected validateEmail(): boolean { + this.formGroup.controls.email.markAsTouched(); + this.formGroup.controls.email.updateValueAndValidity({ onlySelf: true, emitEvent: true }); + return this.formGroup.controls.email.valid; + } + /** * Persist the entered email address and the user's choice to remember it to state. */ private async persistEmailIfValid(): Promise { - if (await this.emailIsValid()) { + if (this.formGroup.controls.email.valid) { const email = this.formGroup.value.email; const rememberEmail = this.formGroup.value.rememberEmail ?? false; if (!email) { @@ -613,7 +617,7 @@ export class LoginComponent implements OnInit, OnDestroy { } /** - * Set the email value from the input field. + * Set the email value from the input field and persists to state if valid. * We only update the form controls onSubmit instead of onBlur because we don't want to show validation errors until * the user submits. This is because currently our validation errors are shown below the input fields, and * displaying them causes the screen to "jump". @@ -626,7 +630,7 @@ export class LoginComponent implements OnInit, OnDestroy { } /** - * Set the Remember Email value from the input field. + * Set the Remember Email value from the input field and persists to state if valid. * We only update the form controls onSubmit instead of onBlur because we don't want to show validation errors until * the user submits. This is because currently our validation errors are shown below the input fields, and * displaying them causes the screen to "jump". From 938e9454e140668d51dd03e81fac58c0a9c53d3a Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Mon, 14 Apr 2025 21:33:51 -0400 Subject: [PATCH 19/19] fix(workflow): [PM-19254] Update image tag generation for builds from forked PRs * Added fork name to tag * Added logging. * Added pull_request_target * Added repository name if on fork. * Limited characters * Added sanitization * Moved to env var for extra security. --- .github/workflows/build-web.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index 12748a47748..3da524702fe 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -45,7 +45,7 @@ on: env: _AZ_REGISTRY: bitwardenprod.azurecr.io - + _GITHUB_PR_REPO_NAME: ${{ github.event.pull_request.head.repo.full_name }} jobs: setup: @@ -190,12 +190,18 @@ jobs: - name: Generate container image tag id: tag run: | - if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then - IMAGE_TAG=$(echo "${GITHUB_HEAD_REF}" | sed "s#/#-#g") + if [[ "${GITHUB_EVENT_NAME}" == "pull_request" || "${GITHUB_EVENT_NAME}" == "pull_request_target" ]]; then + IMAGE_TAG=$(echo "${GITHUB_HEAD_REF}" | sed "s/[^a-zA-Z0-9]/-/g") # Sanitize branch name to alphanumeric only else IMAGE_TAG=$(echo "${GITHUB_REF_NAME}" | sed "s#/#-#g") fi + if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then + SANITIZED_REPO_NAME=$(echo "$_GITHUB_PR_REPO_NAME" | sed "s/[^a-zA-Z0-9]/-/g") # Sanitize repo name to alphanumeric only + IMAGE_TAG=$SANITIZED_REPO_NAME-$IMAGE_TAG # Add repo name to the tag + IMAGE_TAG=${IMAGE_TAG:0:128} # Limit to 128 characters, as that's the max length for Docker image tags + fi + if [[ "$IMAGE_TAG" == "main" ]]; then IMAGE_TAG=dev fi