diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 354fd4eddab..2c28d0cb523 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -127,7 +127,9 @@ jobs: # Note, before updating the ubuntu version of the workflow, ensure the snap base image # is equal or greater than the new version. Otherwise there might be GLIBC version issues. # The snap base for desktop is defined in `apps/desktop/electron-builder.json` - runs-on: ubuntu-22.04 + # We are currently running on 20.04 until the Ubuntu 24.04 release is available, as moving + # to 22.04 now breaks users who are on 20.04 due to mismatched GLIBC versions. + runs-on: ubuntu-20.04 needs: setup env: _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} diff --git a/apps/browser/package.json b/apps/browser/package.json index 1ea03576016..b03469bbb72 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2024.2.1", + "version": "2024.3.0", "scripts": { "build": "webpack", "build:mv3": "cross-env MANIFEST_VERSION=3 webpack", diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index c1a88913aaf..55c6042ae83 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -360,7 +360,7 @@ "message": "Gwefan" }, "toggleVisibility": { - "message": "Toggle visibility" + "message": "Toglo gwelededd" }, "manage": { "message": "Rheoli" @@ -381,7 +381,7 @@ "message": "Ystyriwch ein helpu ni gydag adolygiad da!" }, "browserNotSupportClipboard": { - "message": "Your web browser does not support easy clipboard copying. Copy it manually instead." + "message": "Dyw eich porwr gwe ddim yn cefnogi copïo drwy'r clipfwrdd yn hawdd. Copïwch â llaw yn lle." }, "verifyIdentity": { "message": "Gwirio'ch hunaniaeth" @@ -415,7 +415,7 @@ "message": "Cloi nawr" }, "lockAll": { - "message": "Lock all" + "message": "Cloi'r cwbl" }, "immediately": { "message": "ar unwaith" @@ -2925,7 +2925,7 @@ "message": "Newid i gyfrif" }, "activeAccount": { - "message": "Active account" + "message": "Cyfrif gweithredol" }, "availableAccounts": { "message": "Cyfrifon ar gael" @@ -2943,7 +2943,7 @@ "message": "unlocked" }, "server": { - "message": "server" + "message": "gweinydd" }, "hostedAt": { "message": "hosted at" @@ -2952,7 +2952,7 @@ "message": "Use your device or hardware key" }, "justOnce": { - "message": "Just once" + "message": "Unwaith yn unig" }, "alwaysForThisSite": { "message": "Always for this site" @@ -2971,7 +2971,7 @@ "description": "Label indicating the most common import formats" }, "overrideDefaultBrowserAutofillTitle": { - "message": "Make Bitwarden your default password manager?", + "message": "Hoffech chi wneud Bitwarden yn rheolydd cyfrineiriau rhagosodedig?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index b377809ab1c..7a013add6e7 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -2709,7 +2709,7 @@ "message": "Starte DUO und folge den Schritten, um die Anmeldung zu abzuschließen." }, "duoRequiredForAccount": { - "message": "Duo two-step login is required for your account." + "message": "Für dein Konto ist die Duo Zwei-Faktor-Authentifizierung erforderlich." }, "popoutTheExtensionToCompleteLogin": { "message": "Popout the extension to complete login." diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index 471c1981d7a..58172b1a7af 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -2005,7 +2005,7 @@ "message": "Seleccione carpeta..." }, "noFoldersFound": { - "message": "No folders found", + "message": "Ninguna carpeta encontrada", "description": "Used as a message within the notification bar when no folders are found" }, "orgPermissionsUpdatedMustSetPassword": { @@ -2670,25 +2670,25 @@ "message": "Verificar biométricamente" }, "awaitingConfirmation": { - "message": "Awaiting confirmation" + "message": "Esperando confirmación" }, "couldNotCompleteBiometrics": { - "message": "Could not complete biometrics." + "message": "No se pudo completar la biométrica." }, "needADifferentMethod": { "message": "¿Necesita un método distinto?" }, "useMasterPassword": { - "message": "Use master password" + "message": "Usar contraseña maestra" }, "usePin": { "message": "Usar NIP" }, "useBiometrics": { - "message": "Use biometrics" + "message": "Usar biométrica" }, "enterVerificationCodeSentToEmail": { - "message": "Enter the verification code that was sent to your email." + "message": "Introduzca el código de verificación que se ha enviado a su correo electrónico." }, "resendCode": { "message": "Volver a enviar código" @@ -2706,19 +2706,19 @@ } }, "launchDuoAndFollowStepsToFinishLoggingIn": { - "message": "Launch Duo and follow the steps to finish logging in." + "message": "Abra Duo y siga los pasos para terminar de iniciar sesión." }, "duoRequiredForAccount": { - "message": "Duo two-step login is required for your account." + "message": "Se requiere el inicio de sesión en dos pasos Duo para su cuenta." }, "popoutTheExtensionToCompleteLogin": { - "message": "Popout the extension to complete login." + "message": "Abra la extensión para completar el inicio de sesión." }, "popoutExtension": { - "message": "Popout extension" + "message": "Abrir extensión" }, "launchDuo": { - "message": "Launch Duo" + "message": "Iniciar Duo" }, "importFormatError": { "message": "Los datos no están formateados correctamente. Por favor, comprueba tu archivo de importación e inténtalo de nuevo." @@ -2995,15 +2995,15 @@ "description": "Button text for the setting that allows overriding the default browser autofill settings" }, "saveCipherAttemptSuccess": { - "message": "Credentials saved successfully!", + "message": "¡Credenciales guardadas con éxito!", "description": "Notification message for when saving credentials has succeeded." }, "updateCipherAttemptSuccess": { - "message": "Credentials updated successfully!", + "message": "¡Credenciales actualizadas con éxito!", "description": "Notification message for when updating credentials has succeeded." }, "saveCipherAttemptFailed": { - "message": "Error saving credentials. Check console for details.", + "message": "Se produjo un error al guardar las credenciales. Revise la consola para obtener detalles.", "description": "Notification message for when saving credentials has failed." } } diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index a4186df93bf..2943c7234d9 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -92,13 +92,13 @@ "message": "자동 완성" }, "autoFillLogin": { - "message": "Auto-fill login" + "message": "로그인 자동 완성" }, "autoFillCard": { - "message": "Auto-fill card" + "message": "카드 자동 완성" }, "autoFillIdentity": { - "message": "Auto-fill identity" + "message": "신원 자동 완성" }, "generatePasswordCopied": { "message": "비밀번호 생성 및 클립보드에 복사" @@ -110,19 +110,19 @@ "message": "사용할 수 있는 로그인이 없습니다." }, "noCards": { - "message": "No cards" + "message": "카드 없음" }, "noIdentities": { - "message": "No identities" + "message": "신원 없음" }, "addLoginMenu": { - "message": "Add login" + "message": "로그인 추가" }, "addCardMenu": { - "message": "Add card" + "message": "카드 추가" }, "addIdentityMenu": { - "message": "Add identity" + "message": "신원 추가" }, "unlockVaultMenu": { "message": "보관함 잠금 해제" @@ -220,7 +220,7 @@ "message": "도움말 및 의견" }, "helpCenter": { - "message": "Bitwarden Help center" + "message": "Bitwarden 도움말 센터" }, "communityForums": { "message": "Explore Bitwarden community forums" diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index dd93a93fc9a..cd189db75f8 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -1500,7 +1500,7 @@ "message": "无效 PIN 码。" }, "tooManyInvalidPinEntryAttemptsLoggingOut": { - "message": "无效的 PIN 输入尝试次数过多,正在注销。" + "message": "无效的 PIN 输入尝试次数过多,正在退出登录。" }, "unlockWithBiometrics": { "message": "使用生物识别解锁" diff --git a/apps/browser/src/admin-console/background/service-factories/policy-service.factory.ts b/apps/browser/src/admin-console/background/service-factories/policy-service.factory.ts index efdb743196f..b00693bd564 100644 --- a/apps/browser/src/admin-console/background/service-factories/policy-service.factory.ts +++ b/apps/browser/src/admin-console/background/service-factories/policy-service.factory.ts @@ -1,4 +1,5 @@ import { PolicyService as AbstractPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; import { CachedServices, @@ -9,11 +10,6 @@ import { stateProviderFactory, StateProviderInitOptions, } from "../../../platform/background/service-factories/state-provider.factory"; -import { - stateServiceFactory as stateServiceFactory, - StateServiceInitOptions, -} from "../../../platform/background/service-factories/state-service.factory"; -import { BrowserPolicyService } from "../../services/browser-policy.service"; import { organizationServiceFactory, @@ -23,7 +19,6 @@ import { type PolicyServiceFactoryOptions = FactoryOptions; export type PolicyServiceInitOptions = PolicyServiceFactoryOptions & - StateServiceInitOptions & StateProviderInitOptions & OrganizationServiceInitOptions; @@ -36,8 +31,7 @@ export function policyServiceFactory( "policyService", opts, async () => - new BrowserPolicyService( - await stateServiceFactory(cache, opts), + new PolicyService( await stateProviderFactory(cache, opts), await organizationServiceFactory(cache, opts), ), diff --git a/apps/browser/src/admin-console/services/browser-policy.service.ts b/apps/browser/src/admin-console/services/browser-policy.service.ts deleted file mode 100644 index 2022cfec583..00000000000 --- a/apps/browser/src/admin-console/services/browser-policy.service.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { BehaviorSubject } from "rxjs"; -import { Jsonify } from "type-fest"; - -import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; -import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; - -import { browserSession, sessionSync } from "../../platform/decorators/session-sync-observable"; - -@browserSession -export class BrowserPolicyService extends PolicyService { - @sessionSync({ - initializer: (obj: Jsonify) => Object.assign(new Policy(), obj), - initializeAs: "array", - }) - protected _policies: BehaviorSubject; -} diff --git a/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts b/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts index f2b17d2f08a..b827788d75c 100644 --- a/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts +++ b/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts @@ -1,6 +1,8 @@ import { mock, MockProxy } from "jest-mock-extended"; +import { of } from "rxjs"; import { NOOP_COMMAND_SUFFIX } from "@bitwarden/common/autofill/constants"; +import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -13,6 +15,7 @@ import { MainContextMenuHandler } from "./main-context-menu-handler"; describe("context-menu", () => { let stateService: MockProxy; + let autofillSettingsService: MockProxy; let i18nService: MockProxy; let logService: MockProxy; @@ -26,6 +29,7 @@ describe("context-menu", () => { beforeEach(() => { stateService = mock(); + autofillSettingsService = mock(); i18nService = mock(); logService = mock(); @@ -41,14 +45,20 @@ describe("context-menu", () => { }); i18nService.t.mockImplementation((key) => key); - sut = new MainContextMenuHandler(stateService, i18nService, logService); + sut = new MainContextMenuHandler( + stateService, + autofillSettingsService, + i18nService, + logService, + ); + autofillSettingsService.enableContextMenu$ = of(true); }); afterEach(() => jest.resetAllMocks()); describe("init", () => { it("has menu disabled", async () => { - stateService.getDisableContextMenuItem.mockResolvedValue(true); + autofillSettingsService.enableContextMenu$ = of(false); const createdMenu = await sut.init(); expect(createdMenu).toBeFalsy(); @@ -56,8 +66,6 @@ describe("context-menu", () => { }); it("has menu enabled, but does not have premium", async () => { - stateService.getDisableContextMenuItem.mockResolvedValue(false); - stateService.getCanAccessPremium.mockResolvedValue(false); const createdMenu = await sut.init(); @@ -66,8 +74,6 @@ describe("context-menu", () => { }); it("has menu enabled and has premium", async () => { - stateService.getDisableContextMenuItem.mockResolvedValue(false); - stateService.getCanAccessPremium.mockResolvedValue(true); const createdMenu = await sut.init(); diff --git a/apps/browser/src/autofill/browser/main-context-menu-handler.ts b/apps/browser/src/autofill/browser/main-context-menu-handler.ts index 19154fbfb81..998b5c7258b 100644 --- a/apps/browser/src/autofill/browser/main-context-menu-handler.ts +++ b/apps/browser/src/autofill/browser/main-context-menu-handler.ts @@ -1,3 +1,5 @@ +import { firstValueFrom } from "rxjs"; + import { AUTOFILL_CARD_ID, AUTOFILL_ID, @@ -14,6 +16,7 @@ import { ROOT_ID, SEPARATOR_ID, } from "@bitwarden/common/autofill/constants"; +import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; @@ -22,6 +25,7 @@ import { GlobalState } from "@bitwarden/common/platform/models/domain/global-sta import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { autofillSettingsServiceFactory } from "../../autofill/background/service_factories/autofill-settings-service.factory"; import { Account } from "../../models/account"; import { CachedServices } from "../../platform/background/service-factories/factory-options"; import { @@ -156,6 +160,7 @@ export class MainContextMenuHandler { constructor( private stateService: BrowserStateService, + private autofillSettingsService: AutofillSettingsServiceAbstraction, private i18nService: I18nService, private logService: LogService, ) {} @@ -183,6 +188,7 @@ export class MainContextMenuHandler { return new MainContextMenuHandler( await stateServiceFactory(cachedServices, serviceOptions), + await autofillSettingsServiceFactory(cachedServices, serviceOptions), await i18nServiceFactory(cachedServices, serviceOptions), await logServiceFactory(cachedServices, serviceOptions), ); @@ -193,8 +199,8 @@ export class MainContextMenuHandler { * @returns a boolean showing whether or not items were created */ async init(): Promise { - const menuDisabled = await this.stateService.getDisableContextMenuItem(); - if (menuDisabled) { + const menuEnabled = await firstValueFrom(this.autofillSettingsService.enableContextMenu$); + if (!menuEnabled) { await MainContextMenuHandler.removeAll(); return false; } diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts index 4b3641208b0..8926f5b298e 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts @@ -1214,7 +1214,10 @@ describe("AutofillOverlayContentService", () => { autofillOverlayContentService as any, "removeAutofillOverlay", ); + autofillOverlayContentService["mostRecentlyFocusedField"] = undefined; + autofillOverlayContentService["overlayButtonElement"] = document.createElement("div"); + autofillOverlayContentService["overlayListElement"] = document.createElement("div"); globalThis.dispatchEvent(new Event(EVENTS.SCROLL)); jest.advanceTimersByTime(800); @@ -1222,8 +1225,8 @@ describe("AutofillOverlayContentService", () => { expect(sendExtensionMessageSpy).toHaveBeenCalledWith("updateAutofillOverlayHidden", { display: "block", }); - expect(autofillOverlayContentService["isOverlayButtonVisible"]).toEqual(true); - expect(autofillOverlayContentService["isOverlayListVisible"]).toEqual(true); + expect(autofillOverlayContentService["isOverlayButtonVisible"]).toEqual(false); + expect(autofillOverlayContentService["isOverlayListVisible"]).toEqual(false); expect(removeAutofillOverlaySpy).toHaveBeenCalled(); }); @@ -1280,6 +1283,32 @@ describe("AutofillOverlayContentService", () => { expect(removeAutofillOverlaySpy).toHaveBeenCalled(); }); + + it("defaults overlay elements to a visibility of `false` if the element is not rendered on the page", async () => { + jest.useFakeTimers(); + jest + .spyOn(autofillOverlayContentService as any, "updateMostRecentlyFocusedField") + .mockImplementation(() => { + autofillOverlayContentService["focusedFieldData"] = { + focusedFieldRects: { + top: 100, + }, + focusedFieldStyles: {}, + }; + }); + jest + .spyOn(autofillOverlayContentService as any, "updateOverlayElementsPosition") + .mockImplementation(); + autofillOverlayContentService["overlayButtonElement"] = document.createElement("div"); + autofillOverlayContentService["overlayListElement"] = undefined; + + globalThis.dispatchEvent(new Event(EVENTS.SCROLL)); + jest.advanceTimersByTime(800); + await flushPromises(); + + expect(autofillOverlayContentService["isOverlayButtonVisible"]).toEqual(true); + expect(autofillOverlayContentService["isOverlayListVisible"]).toEqual(false); + }); }); describe("handleOverlayElementMutationObserverUpdate", () => { diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts index efbc9732b6c..2cf063a5ba8 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -646,12 +646,10 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte */ private toggleOverlayHidden(isHidden: boolean) { const displayValue = isHidden ? "none" : "block"; - // 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.sendExtensionMessage("updateAutofillOverlayHidden", { display: displayValue }); + void this.sendExtensionMessage("updateAutofillOverlayHidden", { display: displayValue }); - this.isOverlayButtonVisible = !isHidden; - this.isOverlayListVisible = !isHidden; + this.isOverlayButtonVisible = !!this.overlayButtonElement && !isHidden; + this.isOverlayListVisible = !!this.overlayListElement && !isHidden; } /** diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 3c55f53d35e..ab407a81de8 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -22,6 +22,7 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs import { InternalPolicyService as InternalPolicyServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { ProviderService as ProviderServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service"; +import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service"; import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service"; @@ -175,7 +176,6 @@ import { } from "@bitwarden/vault-export-core"; import { BrowserOrganizationService } from "../admin-console/services/browser-organization.service"; -import { BrowserPolicyService } from "../admin-console/services/browser-policy.service"; import ContextMenusBackground from "../autofill/background/context-menus.background"; import NotificationBackground from "../autofill/background/notification.background"; import OverlayBackground from "../autofill/background/overlay.background"; @@ -195,12 +195,12 @@ import { BrowserStateService as StateServiceAbstraction } from "../platform/serv import { BrowserConfigService } from "../platform/services/browser-config.service"; import { BrowserCryptoService } from "../platform/services/browser-crypto.service"; import { BrowserEnvironmentService } from "../platform/services/browser-environment.service"; -import { BrowserI18nService } from "../platform/services/browser-i18n.service"; import BrowserLocalStorageService from "../platform/services/browser-local-storage.service"; import BrowserMessagingPrivateModeBackgroundService from "../platform/services/browser-messaging-private-mode-background.service"; import BrowserMessagingService from "../platform/services/browser-messaging.service"; import BrowserPlatformUtilsService from "../platform/services/browser-platform-utils.service"; import { BrowserStateService } from "../platform/services/browser-state.service"; +import I18nService from "../platform/services/i18n.service"; import { LocalBackedSessionStorageService } from "../platform/services/local-backed-session-storage.service"; import { BackgroundDerivedStateProvider } from "../platform/state/background-derived-state.provider"; import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service"; @@ -352,7 +352,7 @@ export default class MainBackground { this.cryptoFunctionService = new WebCryptoFunctionService(self); this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService); this.storageService = new BrowserLocalStorageService(); - this.secureStorageService = new BrowserLocalStorageService(); + this.secureStorageService = this.storageService; // secure storage is not supported in browsers, so we use local storage and warn users when it is used this.memoryStorageService = BrowserApi.isManifestVersion(3) ? new LocalBackedSessionStorageService( new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false), @@ -462,7 +462,7 @@ export default class MainBackground { }, self, ); - this.i18nService = new BrowserI18nService(BrowserApi.getUILanguage(), this.stateService); + this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider); this.cryptoService = new BrowserCryptoService( this.keyGenerationService, this.cryptoFunctionService, @@ -501,11 +501,7 @@ export default class MainBackground { this.stateService, this.stateProvider, ); - this.policyService = new BrowserPolicyService( - this.stateService, - this.stateProvider, - this.organizationService, - ); + this.policyService = new PolicyService(this.stateProvider, this.organizationService); this.autofillSettingsService = new AutofillSettingsService( this.stateProvider, this.policyService, @@ -819,6 +815,7 @@ export default class MainBackground { this.stateService, this.autofillSettingsService, this.vaultTimeoutSettingsService, + this.biometricStateService, ); // Other fields @@ -954,6 +951,7 @@ export default class MainBackground { if (!this.popupOnlyContext) { this.mainContextMenuHandler = new MainContextMenuHandler( this.stateService, + this.autofillSettingsService, this.i18nService, this.logService, ); @@ -972,7 +970,7 @@ export default class MainBackground { await this.stateService.init(); await this.vaultTimeoutService.init(true); - await (this.i18nService as BrowserI18nService).init(); + await (this.i18nService as I18nService).init(); await (this.eventUploadService as EventUploadService).init(true); await this.runtimeBackground.init(); await this.notificationBackground.init(); diff --git a/apps/browser/src/background/nativeMessaging.background.ts b/apps/browser/src/background/nativeMessaging.background.ts index e4fb46d960f..2717a7b2b56 100644 --- a/apps/browser/src/background/nativeMessaging.background.ts +++ b/apps/browser/src/background/nativeMessaging.background.ts @@ -84,10 +84,6 @@ export class NativeMessagingBackground { private authService: AuthService, private biometricStateService: BiometricStateService, ) { - // 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.stateService.setBiometricFingerprintValidated(false); - if (chrome?.permissions?.onAdded) { // Reload extension to activate nativeMessaging chrome.permissions.onAdded.addListener((permissions) => { @@ -100,9 +96,7 @@ export class NativeMessagingBackground { async connect() { this.appId = await this.appIdService.getAppId(); - // 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.stateService.setBiometricFingerprintValidated(false); + await this.biometricStateService.setFingerprintValidated(false); return new Promise((resolve, reject) => { this.port = BrowserApi.connectNative("com.8bit.bitwarden"); @@ -148,9 +142,7 @@ export class NativeMessagingBackground { if (this.validatingFingerprint) { this.validatingFingerprint = false; - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.stateService.setBiometricFingerprintValidated(true); + await this.biometricStateService.setFingerprintValidated(true); } this.sharedSecret = new SymmetricCryptoKey(decrypted); this.secureSetupResolve(); diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index f56395cd999..e3f6e6353f3 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.2.1", + "version": "2024.3.0", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index de4137ec244..25215597f8c 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "102.0", "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.2.1", + "version": "2024.3.0", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/platform/background/service-factories/i18n-service.factory.ts b/apps/browser/src/platform/background/service-factories/i18n-service.factory.ts index 86ec82784b1..9f9580df843 100644 --- a/apps/browser/src/platform/background/service-factories/i18n-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/i18n-service.factory.ts @@ -4,6 +4,10 @@ import { I18nService as BaseI18nService } from "@bitwarden/common/platform/servi import I18nService from "../../services/i18n.service"; import { FactoryOptions, CachedServices, factory } from "./factory-options"; +import { + GlobalStateProviderInitOptions, + globalStateProviderFactory, +} from "./global-state-provider.factory"; type I18nServiceFactoryOptions = FactoryOptions & { i18nServiceOptions: { @@ -11,7 +15,7 @@ type I18nServiceFactoryOptions = FactoryOptions & { }; }; -export type I18nServiceInitOptions = I18nServiceFactoryOptions; +export type I18nServiceInitOptions = I18nServiceFactoryOptions & GlobalStateProviderInitOptions; export async function i18nServiceFactory( cache: { i18nService?: AbstractI18nService } & CachedServices, @@ -21,7 +25,11 @@ export async function i18nServiceFactory( cache, "i18nService", opts, - () => new I18nService(opts.i18nServiceOptions.systemLanguage), + async () => + new I18nService( + opts.i18nServiceOptions.systemLanguage, + await globalStateProviderFactory(cache, opts), + ), ); if (!(service as BaseI18nService as any).inited) { await (service as BaseI18nService).init(); diff --git a/apps/browser/src/platform/services/browser-i18n.service.ts b/apps/browser/src/platform/services/browser-i18n.service.ts deleted file mode 100644 index 66821bc1582..00000000000 --- a/apps/browser/src/platform/services/browser-i18n.service.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ReplaySubject } from "rxjs"; - -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; - -import { browserSession, sessionSync } from "../decorators/session-sync-observable"; - -import I18nService from "./i18n.service"; - -@browserSession -export class BrowserI18nService extends I18nService { - @sessionSync({ initializer: (s: string) => s }) - protected _locale: ReplaySubject; - - constructor( - systemLanguage: string, - private stateService: StateService, - ) { - super(systemLanguage); - } -} diff --git a/apps/browser/src/platform/services/i18n.service.ts b/apps/browser/src/platform/services/i18n.service.ts index 1badfdb7cb2..334ad8dc6cf 100644 --- a/apps/browser/src/platform/services/i18n.service.ts +++ b/apps/browser/src/platform/services/i18n.service.ts @@ -1,12 +1,18 @@ import { I18nService as BaseI18nService } from "@bitwarden/common/platform/services/i18n.service"; +import { GlobalStateProvider } from "@bitwarden/common/platform/state"; export default class I18nService extends BaseI18nService { - constructor(systemLanguage: string) { - super(systemLanguage, null, async (formattedLocale: string) => { - // Deprecated - const file = await fetch(this.localesDirectory + formattedLocale + "/messages.json"); - return await file.json(); - }); + constructor(systemLanguage: string, globalStateProvider: GlobalStateProvider) { + super( + systemLanguage, + null, + async (formattedLocale: string) => { + // Deprecated + const file = await fetch(this.localesDirectory + formattedLocale + "/messages.json"); + return await file.json(); + }, + globalStateProvider, + ); // Please leave 'en' where it is, as it's our fallback language in case no translation can be found this.supportedTranslationLocales = [ diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index d4c8ec57fda..a7d69df5442 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -1,4 +1,4 @@ -import { APP_INITIALIZER, LOCALE_ID, NgModule, NgZone } from "@angular/core"; +import { APP_INITIALIZER, NgModule, NgZone } from "@angular/core"; import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/auth/guards"; import { ThemingService } from "@bitwarden/angular/platform/services/theming/theming.service"; @@ -60,10 +60,7 @@ import { } 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 { - StateService as BaseStateServiceAbstraction, - StateService, -} from "@bitwarden/common/platform/abstractions/state.service"; +import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { AbstractMemoryStorageService, AbstractStorageService, @@ -75,7 +72,11 @@ import { ConsoleLogService } from "@bitwarden/common/platform/services/console-l import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service"; -import { DerivedStateProvider, StateProvider } from "@bitwarden/common/platform/state"; +import { + DerivedStateProvider, + GlobalStateProvider, + StateProvider, +} from "@bitwarden/common/platform/state"; import { SearchService } from "@bitwarden/common/services/search.service"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username"; @@ -102,7 +103,6 @@ import { ImportServiceAbstraction } from "@bitwarden/importer/core"; import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core"; import { BrowserOrganizationService } from "../../admin-console/services/browser-organization.service"; -import { BrowserPolicyService } from "../../admin-console/services/browser-policy.service"; import { UnauthGuardService } from "../../auth/popup/services"; import { AutofillService } from "../../autofill/services/abstractions/autofill.service"; import MainBackground from "../../background/main.background"; @@ -113,11 +113,11 @@ import { BrowserStateService as StateServiceAbstraction } from "../../platform/s import { BrowserConfigService } from "../../platform/services/browser-config.service"; import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service"; import { BrowserFileDownloadService } from "../../platform/services/browser-file-download.service"; -import { BrowserI18nService } from "../../platform/services/browser-i18n.service"; import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service"; import BrowserMessagingPrivateModePopupService from "../../platform/services/browser-messaging-private-mode-popup.service"; import BrowserMessagingService from "../../platform/services/browser-messaging.service"; import { BrowserStateService } from "../../platform/services/browser-state.service"; +import I18nService from "../../platform/services/i18n.service"; import { ForegroundDerivedStateProvider } from "../../platform/state/foreground-derived-state.provider"; import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service"; import { BrowserSendService } from "../../services/browser-send.service"; @@ -159,11 +159,6 @@ function getBgService(service: keyof MainBackground) { DebounceNavigationService, DialogService, PopupCloseWarningService, - { - provide: LOCALE_ID, - useFactory: () => getBgService("i18nService")().translationLocale, - deps: [], - }, { provide: APP_INITIALIZER, useFactory: (initService: InitService) => initService.init(), @@ -220,10 +215,6 @@ function getBgService(service: keyof MainBackground) { useFactory: () => new WebCryptoFunctionService(window), deps: [], }, - { - provide: FileUploadService, - useFactory: getBgService("fileUploadService"), - }, { provide: InternalFolderService, useExisting: FolderServiceAbstraction, @@ -242,8 +233,9 @@ function getBgService(service: keyof MainBackground) { }, { provide: LogServiceAbstraction, - useFactory: getBgService("logService"), - deps: [], + useFactory: (platformUtilsService: PlatformUtilsService) => + new ConsoleLogService(platformUtilsService.isDev()), + deps: [PlatformUtilsService], }, { provide: BrowserEnvironmentService, @@ -258,10 +250,10 @@ function getBgService(service: keyof MainBackground) { { provide: TokenService, useFactory: getBgService("tokenService"), deps: [] }, { provide: I18nServiceAbstraction, - useFactory: (stateService: BrowserStateService) => { - return new BrowserI18nService(BrowserApi.getUILanguage(), stateService); + useFactory: (globalStateProvider: GlobalStateProvider) => { + return new I18nService(BrowserApi.getUILanguage(), globalStateProvider); }, - deps: [StateService], + deps: [GlobalStateProvider], }, { provide: CryptoService, @@ -297,17 +289,6 @@ function getBgService(service: keyof MainBackground) { useFactory: getBgService("eventCollectionService"), deps: [], }, - { - provide: PolicyService, - useFactory: ( - stateService: StateServiceAbstraction, - stateProvider: StateProvider, - organizationService: OrganizationService, - ) => { - return new BrowserPolicyService(stateService, stateProvider, organizationService); - }, - deps: [StateServiceAbstraction, StateProvider, OrganizationService], - }, { provide: PlatformUtilsService, useFactory: getBgService("platformUtilsService"), @@ -323,7 +304,6 @@ function getBgService(service: keyof MainBackground) { useFactory: getBgService("passwordGenerationService"), deps: [], }, - { provide: ApiService, useFactory: getBgService("apiService"), deps: [] }, { provide: SendService, useFactory: ( @@ -410,11 +390,6 @@ function getBgService(service: keyof MainBackground) { useFactory: getBgService("notificationsService"), deps: [], }, - { - provide: LogServiceAbstraction, - useFactory: getBgService("logService"), - deps: [], - }, { provide: OrganizationService, useFactory: (stateService: StateServiceAbstraction, stateProvider: StateProvider) => { @@ -437,7 +412,7 @@ function getBgService(service: keyof MainBackground) { }, { provide: SECURE_STORAGE, - useFactory: getBgService("secureStorageService"), + useExisting: AbstractStorageService, // Secure storage is not available in the browser, so we use normal storage instead and warn users when it is used. }, { provide: MEMORY_STORAGE, diff --git a/apps/browser/src/popup/settings/options.component.ts b/apps/browser/src/popup/settings/options.component.ts index ac416a34930..2af37131710 100644 --- a/apps/browser/src/popup/settings/options.component.ts +++ b/apps/browser/src/popup/settings/options.component.ts @@ -105,7 +105,9 @@ export class OptionsComponent implements OnInit { this.userNotificationSettingsService.enableChangedPasswordPrompt$, ); - this.enableContextMenuItem = !(await this.stateService.getDisableContextMenuItem()); + this.enableContextMenuItem = await firstValueFrom( + this.autofillSettingsService.enableContextMenu$, + ); this.showCardsCurrentTab = !(await this.stateService.getDontShowCardsCurrentTab()); this.showIdentitiesCurrentTab = !(await this.stateService.getDontShowIdentitiesCurrentTab()); @@ -143,7 +145,7 @@ export class OptionsComponent implements OnInit { } async updateContextMenuItem() { - await this.stateService.setDisableContextMenuItem(!this.enableContextMenuItem); + await this.autofillSettingsService.setEnableContextMenu(this.enableContextMenuItem); this.messagingService.send("bgUpdateContextMenu"); } diff --git a/apps/browser/src/popup/settings/settings.component.ts b/apps/browser/src/popup/settings/settings.component.ts index f622cffd3e4..c83c7b5e72c 100644 --- a/apps/browser/src/popup/settings/settings.component.ts +++ b/apps/browser/src/popup/settings/settings.component.ts @@ -415,7 +415,7 @@ export class SettingsComponent implements OnInit { ]); } else { await this.biometricStateService.setBiometricUnlockEnabled(false); - await this.stateService.setBiometricFingerprintValidated(false); + await this.biometricStateService.setFingerprintValidated(false); } } diff --git a/apps/cli/package.json b/apps/cli/package.json index fe1a9e77ae4..2873be4242b 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/cli", "description": "A secure and free password manager for all of your devices.", - "version": "2024.2.1", + "version": "2024.3.0", "keywords": [ "bitwarden", "password", @@ -71,7 +71,7 @@ "papaparse": "5.4.1", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", - "tldts": "6.1.11", + "tldts": "6.1.13", "zxcvbn": "4.4.2" } } diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 24a95be6285..2485af89e1f 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -231,7 +231,6 @@ export class Main { p = path.join(process.env.HOME, ".config/Bitwarden CLI"); } - this.i18nService = new I18nService("en", "./locales"); this.platformUtilsService = new CliPlatformUtilsService(ClientType.Cli, packageJson); this.logService = new ConsoleLogService( this.platformUtilsService.isDev(), @@ -270,6 +269,8 @@ export class Main { storageServiceProvider, ); + this.i18nService = new I18nService("en", "./locales", this.globalStateProvider); + this.singleUserStateProvider = new DefaultSingleUserStateProvider( storageServiceProvider, stateEventRegistrarService, @@ -394,11 +395,7 @@ export class Main { this.organizationUserService = new OrganizationUserServiceImplementation(this.apiService); - this.policyService = new PolicyService( - this.stateService, - this.stateProvider, - this.organizationService, - ); + this.policyService = new PolicyService(this.stateProvider, this.organizationService); this.policyApiService = new PolicyApiService(this.policyService, this.apiService); @@ -654,7 +651,7 @@ export class Main { this.cipherService.clear(userId), this.folderService.clear(userId), this.collectionService.clear(userId as UserId), - this.policyService.clear(userId), + this.policyService.clear(userId as UserId), this.passwordGenerationService.clear(), this.providerService.save(null, userId as UserId), ]); @@ -670,8 +667,7 @@ export class Main { await this.stateService.init(); this.containerService.attachToGlobal(global); await this.environmentService.setUrlsFromStorage(); - const locale = await this.stateService.getLocale(); - await this.i18nService.init(locale); + await this.i18nService.init(); this.twoFactorService.init(); this.configService.init(); diff --git a/apps/cli/src/platform/services/i18n.service.ts b/apps/cli/src/platform/services/i18n.service.ts index 0a52aba41e9..61e1ed6b09a 100644 --- a/apps/cli/src/platform/services/i18n.service.ts +++ b/apps/cli/src/platform/services/i18n.service.ts @@ -2,18 +2,28 @@ import * as fs from "fs"; import * as path from "path"; import { I18nService as BaseI18nService } from "@bitwarden/common/platform/services/i18n.service"; +import { GlobalStateProvider } from "@bitwarden/common/platform/state"; export class I18nService extends BaseI18nService { - constructor(systemLanguage: string, localesDirectory: string) { - super(systemLanguage, localesDirectory, (formattedLocale: string) => { - const filePath = path.join( - __dirname, - this.localesDirectory + "/" + formattedLocale + "/messages.json", - ); - const localesJson = fs.readFileSync(filePath, "utf8"); - const locales = JSON.parse(localesJson.replace(/^\uFEFF/, "")); // strip the BOM - return Promise.resolve(locales); - }); + constructor( + systemLanguage: string, + localesDirectory: string, + globalStateProvider: GlobalStateProvider, + ) { + super( + systemLanguage, + localesDirectory, + (formattedLocale: string) => { + const filePath = path.join( + __dirname, + this.localesDirectory + "/" + formattedLocale + "/messages.json", + ); + const localesJson = fs.readFileSync(filePath, "utf8"); + const locales = JSON.parse(localesJson.replace(/^\uFEFF/, "")); // strip the BOM + return Promise.resolve(locales); + }, + globalStateProvider, + ); this.supportedTranslationLocales = ["en"]; } diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 8905ddabf8d..085ec3f872f 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -510,9 +510,9 @@ checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" [[package]] name = "gio" -version = "0.18.4" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +checksum = "2eae10b27b6dd27e22ed0d812c6387deba295e6fc004a8b379e459b663b05a02" dependencies = [ "futures-channel", "futures-core", @@ -521,7 +521,6 @@ dependencies = [ "gio-sys", "glib", "libc", - "once_cell", "pin-project-lite", "smallvec", "thiserror", @@ -529,22 +528,22 @@ dependencies = [ [[package]] name = "gio-sys" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +checksum = "bcf8e1d9219bb294636753d307b030c1e8a032062cba74f493c431a5c8b81ce4" dependencies = [ "glib-sys", "gobject-sys", "libc", "system-deps", - "winapi", + "windows-sys 0.52.0", ] [[package]] name = "glib" -version = "0.18.2" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c316afb01ce8067c5eaab1fc4f2cd47dc21ce7b6296358605e2ffab23ccbd19" +checksum = "ab9e86540b5d8402e905ad4ce7d6aa544092131ab564f3102175af176b90a053" dependencies = [ "bitflags 2.4.1", "futures-channel", @@ -558,20 +557,18 @@ dependencies = [ "gobject-sys", "libc", "memchr", - "once_cell", "smallvec", "thiserror", ] [[package]] name = "glib-macros" -version = "0.18.2" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8da903822b136d42360518653fcf154455defc437d3e7a81475bf9a95ff1e47" +checksum = "0f5897ca27a83e4cdc7b4666850bade0a2e73e17689aabafcc9acddad9d823b8" dependencies = [ "heck", "proc-macro-crate", - "proc-macro-error", "proc-macro2", "quote", "syn 2.0.38", @@ -579,9 +576,9 @@ dependencies = [ [[package]] name = "glib-sys" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +checksum = "630f097773d7c7a0bb3258df4e8157b47dc98bbfa0e60ad9ab56174813feced4" dependencies = [ "libc", "system-deps", @@ -589,9 +586,9 @@ dependencies = [ [[package]] name = "gobject-sys" -version = "0.18.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +checksum = "c85e2b1080b9418dd0c58b498da3a5c826030343e0ef07bde6a955d28de54979" dependencies = [ "glib-sys", "libc", @@ -681,9 +678,9 @@ dependencies = [ [[package]] name = "libsecret" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac6fae6ebe590e06ef9d01b125e46b7d4c05ccbd5961f12b4aefe2ecd010220f" +checksum = "50c6ccddc706a38eca477b4d7857acd6c76c7d6fba5d47b4b2e7d800e5a17194" dependencies = [ "gio", "glib", @@ -693,9 +690,9 @@ dependencies = [ [[package]] name = "libsecret-sys" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b716fc5e1c82eb0d28665882628382ab0e0a156a6d73580e33f0ac6ac8d2540" +checksum = "3a1af48e61f1c8e77e9705296f346e45b637754a92348a79b4c62df84d0654c2" dependencies = [ "gio-sys", "glib-sys", @@ -747,9 +744,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "memoffset" @@ -990,36 +987,11 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro-crate" -version = "1.3.1" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" dependencies = [ - "once_cell", - "toml_edit 0.19.15", -] - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", + "toml_edit 0.21.1", ] [[package]] @@ -1252,9 +1224,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.1" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" +checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" [[package]] name = "socket2" @@ -1406,17 +1378,6 @@ dependencies = [ "serde", ] -[[package]] -name = "toml_edit" -version = "0.19.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" -dependencies = [ - "indexmap", - "toml_datetime", - "winnow", -] - [[package]] name = "toml_edit" version = "0.20.7" @@ -1430,6 +1391,17 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_edit" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + [[package]] name = "tree_magic_mini" version = "3.0.3" diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index 5bb0b0831b2..19bcf52f6eb 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -54,5 +54,5 @@ security-framework = "=2.9.2" security-framework-sys = "=2.9.1" [target.'cfg(target_os = "linux")'.dependencies] -gio = "=0.18.4" -libsecret = "=0.4.0" +gio = "=0.19.2" +libsecret = "=0.5.0" diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 83a2179d58c..1bbf0402da8 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -24,7 +24,7 @@ "**/node_modules/argon2/package.json", "**/node_modules/argon2/lib/binding/napi-v3/argon2.node" ], - "electronVersion": "28.2.5", + "electronVersion": "28.2.6", "generateUpdatesFilesForAllChannels": true, "publish": { "provider": "generic", diff --git a/apps/desktop/package.json b/apps/desktop/package.json index ad26e17699b..d2df3e9a7b1 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/desktop", "description": "A secure and free password manager for all of your devices.", - "version": "2024.2.2", + "version": "2024.3.0", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index 32aad980f02..c594d5acedf 100644 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ b/apps/desktop/src/app/accounts/settings.component.ts @@ -264,7 +264,7 @@ export class SettingsComponent implements OnInit { enableDuckDuckGoBrowserIntegration: await this.stateService.getEnableDuckDuckGoBrowserIntegration(), theme: await this.stateService.getTheme(), - locale: (await this.stateService.getLocale()) ?? null, + locale: await firstValueFrom(this.i18nService.locale$), }; this.form.setValue(initialValues, { emitEvent: false }); @@ -553,7 +553,7 @@ export class SettingsComponent implements OnInit { } async saveLocale() { - await this.stateService.setLocale(this.form.value.locale); + await this.i18nService.setLocale(this.form.value.locale); } async saveTheme() { diff --git a/apps/desktop/src/app/services/init.service.ts b/apps/desktop/src/app/services/init.service.ts index 9bbf0b2e37c..56361499ea1 100644 --- a/apps/desktop/src/app/services/init.service.ts +++ b/apps/desktop/src/app/services/init.service.ts @@ -52,8 +52,7 @@ export class InitService { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.syncService.fullSync(true); await this.vaultTimeoutService.init(true); - const locale = await this.stateService.getLocale(); - await (this.i18nService as I18nRendererService).init(locale); + await (this.i18nService as I18nRendererService).init(); (this.eventUploadService as EventUploadService).init(true); this.twoFactorService.init(); setTimeout(() => this.notificationsService.init(), 3000); diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 2fde9744b90..efc93d698b8 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -43,7 +43,7 @@ import { GlobalState } from "@bitwarden/common/platform/models/domain/global-sta import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; import { SystemService } from "@bitwarden/common/platform/services/system.service"; -import { StateProvider } from "@bitwarden/common/platform/state"; +import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/state"; // eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; @@ -104,7 +104,7 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK"); { provide: I18nServiceAbstraction, useClass: I18nRendererService, - deps: [SYSTEM_LANGUAGE, LOCALES_DIRECTORY], + deps: [SYSTEM_LANGUAGE, LOCALES_DIRECTORY, GlobalStateProvider], }, { provide: MessagingServiceAbstraction, @@ -126,6 +126,7 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK"); StateServiceAbstraction, AutofillSettingsServiceAbstraction, VaultTimeoutSettingsService, + BiometricStateService, ], }, { diff --git a/apps/desktop/src/auth/accessibility-cookie.component.html b/apps/desktop/src/auth/accessibility-cookie.component.html index b5de1e766ff..e81f754cd74 100644 --- a/apps/desktop/src/auth/accessibility-cookie.component.html +++ b/apps/desktop/src/auth/accessibility-cookie.component.html @@ -28,7 +28,7 @@ - + diff --git a/apps/desktop/src/auth/accessibility-cookie.component.ts b/apps/desktop/src/auth/accessibility-cookie.component.ts index 5ec0dbfb56b..fc72b1a9d77 100644 --- a/apps/desktop/src/auth/accessibility-cookie.component.ts +++ b/apps/desktop/src/auth/accessibility-cookie.component.ts @@ -2,14 +2,11 @@ import { Component, NgZone } from "@angular/core"; import { UntypedFormControl, UntypedFormGroup, Validators } from "@angular/forms"; import { Router } from "@angular/router"; -import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -const BroadcasterSubscriptionId = "AccessibilityCookieComponent"; - @Component({ selector: "app-accessibility-cookie", templateUrl: "accessibility-cookie.component.html", @@ -27,40 +24,21 @@ export class AccessibilityCookieComponent { protected platformUtilsService: PlatformUtilsService, protected environmentService: EnvironmentService, protected i18nService: I18nService, - private broadcasterService: BroadcasterService, protected ngZone: NgZone, ) {} - async ngOnInit() { - this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => { - this.ngZone.run(() => { - switch (message.command) { - case "windowIsFocused": - if (this.listenForCookie) { - this.listenForCookie = false; - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.checkForCookie(); - } - break; - default: - } - }); - }); - } - registerhCaptcha() { this.platformUtilsService.launchUri("https://www.hcaptcha.com/accessibility"); } - async checkForCookie() { - this.hCaptchaWindow.close(); + async close() { const [cookie] = await ipc.auth.getHcaptchaAccessibilityCookie(); if (cookie) { this.onCookieSavedSuccess(); } else { this.onCookieSavedFailure(); } + await this.router.navigate(["/login"]); } onCookieSavedSuccess() { @@ -89,10 +67,6 @@ export class AccessibilityCookieComponent { return; } this.listenForCookie = true; - this.hCaptchaWindow = window.open(this.accessibilityForm.value.link); - } - - ngOnDestroy() { - this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); + window.open(this.accessibilityForm.value.link, "_blank", "noopener noreferrer"); } } diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index 86fbe3613fe..cb7ca324456 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -404,7 +404,7 @@ "message": "길이" }, "passwordMinLength": { - "message": "Minimum password length" + "message": "최소 비밀번호 길이" }, "uppercase": { "message": "대문자 (A-Z)" @@ -561,10 +561,10 @@ "message": "계정 생성이 완료되었습니다! 이제 로그인하실 수 있습니다." }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "로그인에 성공했습니다." }, "youMayCloseThisWindow": { - "message": "You may close this window" + "message": "이제 창을 닫으실 수 있습니다." }, "masterPassSent": { "message": "마스터 비밀번호 힌트가 담긴 이메일을 보냈습니다." @@ -780,7 +780,7 @@ "message": "문의하기" }, "helpAndFeedback": { - "message": "Help and feedback" + "message": "도움말 및 피드백" }, "getHelp": { "message": "도움말" @@ -1399,7 +1399,7 @@ "message": "잘못된 PIN 코드입니다." }, "tooManyInvalidPinEntryAttemptsLoggingOut": { - "message": "Too many invalid PIN entry attempts. Logging out." + "message": "잘못된 PIN 입력 시도가 너무 많습니다. 로그아웃 합니다." }, "unlockWithWindowsHello": { "message": "Windows Hello를 사용하여 잠금 해제" @@ -1889,7 +1889,7 @@ "message": "Verification required for this action. Set a PIN to continue." }, "setPin": { - "message": "Set PIN" + "message": "PIN 설정" }, "verifyWithBiometrics": { "message": "Verify with biometrics" diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index dbcb85bacd8..e2c8f9c0ad3 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -97,7 +97,6 @@ export class Main { } this.logService = new ElectronLogMainService(null, app.getPath("userData")); - this.i18nService = new I18nMainService("en", "./locales/"); const storageDefaults: any = {}; // Default vault timeout to "on restart", and action to "lock" @@ -112,6 +111,8 @@ export class Main { ); const globalStateProvider = new DefaultGlobalStateProvider(storageServiceProvider); + this.i18nService = new I18nMainService("en", "./locales/", globalStateProvider); + const accountService = new AccountServiceImplementation( new NoopMessagingService(), this.logService, @@ -218,8 +219,7 @@ export class Main { this.migrationRunner.run().then( async () => { await this.windowMain.init(); - const locale = await this.stateService.getLocale(); - await this.i18nService.init(locale != null ? locale : app.getLocale()); + await this.i18nService.init(); this.messagingMain.init(); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/apps/desktop/src/main/window.main.ts b/apps/desktop/src/main/window.main.ts index 3154a8ccc19..644e4d5f7da 100644 --- a/apps/desktop/src/main/window.main.ts +++ b/apps/desktop/src/main/window.main.ts @@ -30,7 +30,7 @@ export class WindowMain { private windowStateChangeTimer: NodeJS.Timeout; private windowStates: { [key: string]: WindowState } = {}; private enableAlwaysOnTop = false; - private session: Electron.Session; + session: Electron.Session; readonly defaultWidth = 950; readonly defaultHeight = 600; diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 663ed211cd2..3c98a7cf7c4 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2024.2.2", + "version": "2024.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2024.2.2", + "version": "2024.3.0", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-native": "file:../desktop_native" diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index 68d1fbe1128..4cd69416557 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -2,7 +2,7 @@ "name": "@bitwarden/desktop", "productName": "Bitwarden", "description": "A secure and free password manager for all of your devices.", - "version": "2024.2.2", + "version": "2024.3.0", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/apps/desktop/src/platform/services/i18n.main.service.ts b/apps/desktop/src/platform/services/i18n.main.service.ts index 0170c934fd8..edf79eccf00 100644 --- a/apps/desktop/src/platform/services/i18n.main.service.ts +++ b/apps/desktop/src/platform/services/i18n.main.service.ts @@ -1,14 +1,22 @@ import * as fs from "fs"; import * as path from "path"; -import { ipcMain } from "electron"; +import { app, ipcMain } from "electron"; import { I18nService as BaseI18nService } from "@bitwarden/common/platform/services/i18n.service"; +import { GlobalStateProvider } from "@bitwarden/common/platform/state"; export class I18nMainService extends BaseI18nService { - constructor(systemLanguage: string, localesDirectory: string) { - super(systemLanguage, localesDirectory, (formattedLocale: string) => - this.readLanguageFile(formattedLocale), + constructor( + systemLanguage: string, + localesDirectory: string, + globalStateProvider: GlobalStateProvider, + ) { + super( + systemLanguage, + localesDirectory, + (formattedLocale: string) => this.readLanguageFile(formattedLocale), + globalStateProvider, ); ipcMain.handle("getLanguageFile", async (event, formattedLocale: string) => @@ -76,6 +84,12 @@ export class I18nMainService extends BaseI18nService { ]; } + override async init(): Promise { + // Set system language to electron language + this.systemLanguage = app.getLocale(); + await super.init(); + } + private readLanguageFile(formattedLocale: string): Promise { // Check that the provided locale only contains letters and dashes and underscores to avoid possible path traversal if (!/^[a-zA-Z_-]+$/.test(formattedLocale)) { diff --git a/apps/desktop/src/platform/services/i18n.renderer.service.ts b/apps/desktop/src/platform/services/i18n.renderer.service.ts index 906f53566e2..87ad8b40183 100644 --- a/apps/desktop/src/platform/services/i18n.renderer.service.ts +++ b/apps/desktop/src/platform/services/i18n.renderer.service.ts @@ -1,10 +1,20 @@ import { I18nService as BaseI18nService } from "@bitwarden/common/platform/services/i18n.service"; +import { GlobalStateProvider } from "@bitwarden/common/platform/state"; export class I18nRendererService extends BaseI18nService { - constructor(systemLanguage: string, localesDirectory: string) { - super(systemLanguage, localesDirectory, (formattedLocale: string) => { - return ipc.platform.getLanguageFile(formattedLocale); - }); + constructor( + systemLanguage: string, + localesDirectory: string, + globalStateProvider: GlobalStateProvider, + ) { + super( + systemLanguage, + localesDirectory, + (formattedLocale: string) => { + return ipc.platform.getLanguageFile(formattedLocale); + }, + globalStateProvider, + ); // Please leave 'en' where it is, as it's our fallback language in case no translation can be found this.supportedTranslationLocales = [ diff --git a/apps/desktop/src/services/electron-main-messaging.service.ts b/apps/desktop/src/services/electron-main-messaging.service.ts index b7e5712a0c4..71e1b1d7d56 100644 --- a/apps/desktop/src/services/electron-main-messaging.service.ts +++ b/apps/desktop/src/services/electron-main-messaging.service.ts @@ -1,16 +1,6 @@ import * as path from "path"; -import { - app, - dialog, - ipcMain, - Menu, - MenuItem, - nativeTheme, - session, - Notification, - shell, -} from "electron"; +import { app, dialog, ipcMain, Menu, MenuItem, nativeTheme, Notification, shell } from "electron"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; @@ -64,7 +54,7 @@ export class ElectronMainMessagingService implements MessagingService { }); ipcMain.handle("getCookie", async (event, options) => { - return await session.defaultSession.cookies.get(options); + return await this.windowMain.session.cookies.get(options); }); ipcMain.handle("loginRequest", async (event, options) => { diff --git a/apps/web/package.json b/apps/web/package.json index 0c1521e441f..3fad4b14ae1 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2024.2.5", + "version": "2024.3.0", "scripts": { "build:oss": "webpack", "build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js", diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html index 27a3f9fc669..46d8c6a8671 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html @@ -16,7 +16,10 @@ > {{ "loading" | i18n }} - +

{{ "inviteUserDesc" | i18n }}

@@ -60,7 +63,10 @@ -
+
- +

{{ "secretsManager" | i18n }}
{{ "userPermissionOverrideHelper" | i18n }}
-
+
@@ -434,7 +440,7 @@ [columnHeader]="'collection' | i18n" [selectorLabelText]="'selectCollections' | i18n" [emptySelectionText]="'noCollectionsAdded' | i18n" - [flexibleCollectionsEnabled]="flexibleCollectionsEnabled" + [flexibleCollectionsEnabled]="organization.flexibleCollections" > diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts index 5461aecfcc1..4d6442e8988 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts @@ -1,7 +1,16 @@ import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; -import { Component, Inject, OnDestroy, OnInit } from "@angular/core"; +import { Component, Inject, OnDestroy } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; -import { combineLatest, of, shareReplay, Subject, switchMap, takeUntil } from "rxjs"; +import { + combineLatest, + firstValueFrom, + Observable, + of, + shareReplay, + Subject, + switchMap, + takeUntil, +} from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; @@ -18,7 +27,6 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { DialogService } from "@bitwarden/components"; -import { flagEnabled } from "../../../../../../utils/flags"; import { CollectionAdminService } from "../../../../../vault/core/collection-admin.service"; import { CollectionAccessSelectionView, @@ -66,7 +74,7 @@ export enum MemberDialogResult { @Component({ templateUrl: "member-dialog.component.html", }) -export class MemberDialogComponent implements OnInit, OnDestroy { +export class MemberDialogComponent implements OnDestroy { loading = true; editMode = false; isRevoked = false; @@ -74,12 +82,10 @@ export class MemberDialogComponent implements OnInit, OnDestroy { access: "all" | "selected" = "selected"; collections: CollectionView[] = []; organizationUserType = OrganizationUserType; - canUseCustomPermissions: boolean; PermissionMode = PermissionMode; - canUseSecretsManager: boolean; showNoMasterPasswordWarning = false; - protected organization: Organization; + protected organization$: Observable; protected collectionAccessItems: AccessItemView[] = []; protected groupAccessItems: AccessItemView[] = []; protected tabIndex: MemberDialogTab; @@ -130,7 +136,6 @@ export class MemberDialogComponent implements OnInit, OnDestroy { private dialogRef: DialogRef, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, - private organizationService: OrganizationService, private formBuilder: FormBuilder, // TODO: We should really look into consolidating naming conventions for these services private collectionAdminService: CollectionAdminService, @@ -139,28 +144,26 @@ export class MemberDialogComponent implements OnInit, OnDestroy { private organizationUserService: OrganizationUserService, private dialogService: DialogService, private configService: ConfigServiceAbstraction, - ) {} + organizationService: OrganizationService, + ) { + this.organization$ = organizationService + .get$(this.params.organizationId) + .pipe(shareReplay({ refCount: true, bufferSize: 1 })); - async ngOnInit() { this.editMode = this.params.organizationUserId != null; this.tabIndex = this.params.initialTab ?? MemberDialogTab.Role; this.title = this.i18nService.t(this.editMode ? "editMember" : "inviteMember"); - const organization$ = of(this.organizationService.get(this.params.organizationId)).pipe( - shareReplay({ refCount: true, bufferSize: 1 }), - ); - const groups$ = organization$.pipe( - switchMap((organization) => { - if (!organization.useGroups) { - return of([] as GroupView[]); - } - - return this.groupService.getAll(this.params.organizationId); - }), + const groups$ = this.organization$.pipe( + switchMap((organization) => + organization.useGroups + ? this.groupService.getAll(this.params.organizationId) + : of([] as GroupView[]), + ), ); combineLatest({ - organization: organization$, + organization: this.organization$, collections: this.collectionAdminService.getAll(this.params.organizationId), userDetails: this.params.organizationUserId ? this.userService.get(this.params.organizationId, this.params.organizationUserId) @@ -169,23 +172,7 @@ export class MemberDialogComponent implements OnInit, OnDestroy { }) .pipe(takeUntil(this.destroy$)) .subscribe(({ organization, collections, userDetails, groups }) => { - this.organization = organization; - this.canUseCustomPermissions = organization.useCustomPermissions; - this.canUseSecretsManager = organization.useSecretsManager && flagEnabled("secretsManager"); - - const emailsControlValidators = [ - Validators.required, - commaSeparatedEmails, - orgSeatLimitReachedValidator( - this.organization, - this.params.allOrganizationUserEmails, - this.i18nService.t("subscriptionUpgrade", organization.seats), - ), - ]; - - const emailsControl = this.formGroup.get("emails"); - emailsControl.setValidators(emailsControlValidators); - emailsControl.updateValueAndValidity(); + this.setFormValidators(organization); this.collectionAccessItems = [].concat( collections.map((c) => mapCollectionToAccessItemView(c)), @@ -196,77 +183,101 @@ export class MemberDialogComponent implements OnInit, OnDestroy { ); if (this.params.organizationUserId) { - if (!userDetails) { - throw new Error("Could not find user to edit."); - } - this.isRevoked = userDetails.status === OrganizationUserStatusType.Revoked; - this.showNoMasterPasswordWarning = - userDetails.status > OrganizationUserStatusType.Invited && - userDetails.hasMasterPassword === false; - const assignedCollectionsPermissions = { - editAssignedCollections: userDetails.permissions.editAssignedCollections, - deleteAssignedCollections: userDetails.permissions.deleteAssignedCollections, - manageAssignedCollections: - userDetails.permissions.editAssignedCollections && - userDetails.permissions.deleteAssignedCollections, - }; - const allCollectionsPermissions = { - createNewCollections: userDetails.permissions.createNewCollections, - editAnyCollection: userDetails.permissions.editAnyCollection, - deleteAnyCollection: userDetails.permissions.deleteAnyCollection, - manageAllCollections: - userDetails.permissions.createNewCollections && - userDetails.permissions.editAnyCollection && - userDetails.permissions.deleteAnyCollection, - }; - if (userDetails.type === OrganizationUserType.Custom) { - this.permissionsGroup.patchValue({ - accessEventLogs: userDetails.permissions.accessEventLogs, - accessImportExport: userDetails.permissions.accessImportExport, - accessReports: userDetails.permissions.accessReports, - manageGroups: userDetails.permissions.manageGroups, - manageSso: userDetails.permissions.manageSso, - managePolicies: userDetails.permissions.managePolicies, - manageUsers: userDetails.permissions.manageUsers, - manageResetPassword: userDetails.permissions.manageResetPassword, - manageAssignedCollectionsGroup: assignedCollectionsPermissions, - manageAllCollectionsGroup: allCollectionsPermissions, - }); - } - - const collectionsFromGroups = groups - .filter((group) => userDetails.groups.includes(group.id)) - .flatMap((group) => - group.collections.map((accessSelection) => { - const collection = collections.find((c) => c.id === accessSelection.id); - return { group, collection, accessSelection }; - }), - ); - - this.collectionAccessItems = this.collectionAccessItems.concat( - collectionsFromGroups.map(({ collection, accessSelection, group }) => - mapCollectionToAccessItemView(collection, accessSelection, group), - ), - ); - - const accessSelections = mapToAccessSelections(userDetails); - const groupAccessSelections = mapToGroupAccessSelections(userDetails.groups); - - this.formGroup.removeControl("emails"); - this.formGroup.patchValue({ - type: userDetails.type, - externalId: userDetails.externalId, - accessAllCollections: userDetails.accessAll, - access: accessSelections, - accessSecretsManager: userDetails.accessSecretsManager, - groups: groupAccessSelections, - }); + this.loadOrganizationUser(userDetails, groups, collections); } this.loading = false; }); } + private setFormValidators(organization: Organization) { + const emailsControlValidators = [ + Validators.required, + commaSeparatedEmails, + orgSeatLimitReachedValidator( + organization, + this.params.allOrganizationUserEmails, + this.i18nService.t("subscriptionUpgrade", organization.seats), + ), + ]; + + const emailsControl = this.formGroup.get("emails"); + emailsControl.setValidators(emailsControlValidators); + emailsControl.updateValueAndValidity(); + } + + private loadOrganizationUser( + userDetails: OrganizationUserAdminView, + groups: GroupView[], + collections: CollectionView[], + ) { + if (!userDetails) { + throw new Error("Could not find user to edit."); + } + this.isRevoked = userDetails.status === OrganizationUserStatusType.Revoked; + this.showNoMasterPasswordWarning = + userDetails.status > OrganizationUserStatusType.Invited && + userDetails.hasMasterPassword === false; + const assignedCollectionsPermissions = { + editAssignedCollections: userDetails.permissions.editAssignedCollections, + deleteAssignedCollections: userDetails.permissions.deleteAssignedCollections, + manageAssignedCollections: + userDetails.permissions.editAssignedCollections && + userDetails.permissions.deleteAssignedCollections, + }; + const allCollectionsPermissions = { + createNewCollections: userDetails.permissions.createNewCollections, + editAnyCollection: userDetails.permissions.editAnyCollection, + deleteAnyCollection: userDetails.permissions.deleteAnyCollection, + manageAllCollections: + userDetails.permissions.createNewCollections && + userDetails.permissions.editAnyCollection && + userDetails.permissions.deleteAnyCollection, + }; + if (userDetails.type === OrganizationUserType.Custom) { + this.permissionsGroup.patchValue({ + accessEventLogs: userDetails.permissions.accessEventLogs, + accessImportExport: userDetails.permissions.accessImportExport, + accessReports: userDetails.permissions.accessReports, + manageGroups: userDetails.permissions.manageGroups, + manageSso: userDetails.permissions.manageSso, + managePolicies: userDetails.permissions.managePolicies, + manageUsers: userDetails.permissions.manageUsers, + manageResetPassword: userDetails.permissions.manageResetPassword, + manageAssignedCollectionsGroup: assignedCollectionsPermissions, + manageAllCollectionsGroup: allCollectionsPermissions, + }); + } + + const collectionsFromGroups = groups + .filter((group) => userDetails.groups.includes(group.id)) + .flatMap((group) => + group.collections.map((accessSelection) => { + const collection = collections.find((c) => c.id === accessSelection.id); + return { group, collection, accessSelection }; + }), + ); + + this.collectionAccessItems = this.collectionAccessItems.concat( + collectionsFromGroups.map(({ collection, accessSelection, group }) => + mapCollectionToAccessItemView(collection, accessSelection, group), + ), + ); + + const accessSelections = mapToAccessSelections(userDetails); + const groupAccessSelections = mapToGroupAccessSelections(userDetails.groups); + + this.formGroup.removeControl("emails"); + this.formGroup.patchValue({ + type: userDetails.type, + externalId: userDetails.externalId, + accessAllCollections: userDetails.accessAll, + access: accessSelections, + accessSecretsManager: userDetails.accessSecretsManager, + groups: groupAccessSelections, + }); + } + check(c: CollectionView, select?: boolean) { (c as any).checked = select == null ? !(c as any).checked : select; if (!(c as any).checked) { @@ -335,7 +346,9 @@ export class MemberDialogComponent implements OnInit, OnDestroy { return; } - if (!this.canUseCustomPermissions && this.customUserTypeSelected) { + const organization = await firstValueFrom(this.organization$); + + if (!organization.useCustomPermissions && this.customUserTypeSelected) { this.platformUtilsService.showToast( "error", null, @@ -363,8 +376,7 @@ export class MemberDialogComponent implements OnInit, OnDestroy { await this.userService.save(userView); } else { userView.id = this.params.organizationUserId; - const maxEmailsCount = - this.organization.planProductType === ProductType.TeamsStarter ? 10 : 20; + const maxEmailsCount = organization.planProductType === ProductType.TeamsStarter ? 10 : 20; const emails = [...new Set(this.formGroup.value.emails.trim().split(/\s*,\s*/))]; if (emails.length > maxEmailsCount) { this.formGroup.controls.emails.setErrors({ @@ -373,8 +385,8 @@ export class MemberDialogComponent implements OnInit, OnDestroy { return; } if ( - this.organization.hasReseller && - this.params.numConfirmedMembers + emails.length > this.organization.seats + organization.hasReseller && + this.params.numConfirmedMembers + emails.length > organization.seats ) { this.formGroup.controls.emails.setErrors({ tooManyEmails: { message: this.i18nService.t("seatLimitReachedContactYourProvider") }, @@ -515,10 +527,6 @@ export class MemberDialogComponent implements OnInit, OnDestroy { }); } - protected get flexibleCollectionsEnabled() { - return this.organization?.flexibleCollections; - } - protected readonly ProductType = ProductType; } diff --git a/apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.html b/apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.html index 77620677849..a8a4bd53d90 100644 --- a/apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.html +++ b/apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.html @@ -1,52 +1,37 @@ - + {{ "learnMore" | i18n }} +

+

+ {{ fingerprint }} +

+ + + + {{ "dontAskFingerprintAgain" | i18n }} + +
+
+ + +
+ + diff --git a/apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.ts b/apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.ts index 711a7ba9a5b..4afc60c9be3 100644 --- a/apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.ts @@ -1,39 +1,52 @@ -import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; +import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; +import { Component, OnInit, Inject } from "@angular/core"; +import { FormBuilder } from "@angular/forms"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { DialogService } from "@bitwarden/components"; +export enum EmergencyAccessConfirmDialogResult { + Confirmed = "confirmed", +} +type EmergencyAccessConfirmDialogData = { + /** display name of the account requesting emergency access */ + name: string; + /** identifies the account requesting emergency access */ + userId: string; + /** traces a unique emergency request */ + emergencyAccessId: string; +}; @Component({ selector: "emergency-access-confirm", templateUrl: "emergency-access-confirm.component.html", }) export class EmergencyAccessConfirmComponent implements OnInit { - @Input() name: string; - @Input() userId: string; - @Input() emergencyAccessId: string; - @Input() formPromise: Promise; - @Output() onConfirmed = new EventEmitter(); - - dontAskAgain = false; loading = true; fingerprint: string; + confirmForm = this.formBuilder.group({ + dontAskAgain: [false], + }); constructor( + @Inject(DIALOG_DATA) protected params: EmergencyAccessConfirmDialogData, + private formBuilder: FormBuilder, private apiService: ApiService, private cryptoService: CryptoService, private stateService: StateService, private logService: LogService, + private dialogRef: DialogRef, ) {} async ngOnInit() { try { - const publicKeyResponse = await this.apiService.getUserPublicKey(this.userId); + const publicKeyResponse = await this.apiService.getUserPublicKey(this.params.userId); if (publicKeyResponse != null) { const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey); - const fingerprint = await this.cryptoService.getFingerprint(this.userId, publicKey); + const fingerprint = await this.cryptoService.getFingerprint(this.params.userId, publicKey); if (fingerprint != null) { this.fingerprint = fingerprint.join("-"); } @@ -44,19 +57,33 @@ export class EmergencyAccessConfirmComponent implements OnInit { this.loading = false; } - async submit() { + submit = async () => { if (this.loading) { return; } - if (this.dontAskAgain) { + if (this.confirmForm.get("dontAskAgain").value) { await this.stateService.setAutoConfirmFingerprints(true); } try { - this.onConfirmed.emit(); + this.dialogRef.close(EmergencyAccessConfirmDialogResult.Confirmed); } catch (e) { this.logService.error(e); } + }; + /** + * Strongly typed helper to open a EmergencyAccessConfirmComponent + * @param dialogService Instance of the dialog service that will be used to open the dialog + * @param config Configuration for the dialog + */ + static open( + dialogService: DialogService, + config: DialogConfig, + ) { + return dialogService.open( + EmergencyAccessConfirmComponent, + config, + ); } } diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.html b/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.html index 43961d6a13c..1e61585e422 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.html +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.html @@ -1,142 +1,68 @@ - + + + {{ "view" | i18n }} + {{ "viewDesc" | i18n }} + + + + {{ "takeover" | i18n }} + {{ "takeoverDesc" | i18n }} + + + + + {{ "waitTime" | i18n }} + + + + {{ "waitTimeDesc" | i18n }} + + + + + + + + + diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts b/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts index 4aec390a56a..d99c693e73e 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts @@ -1,45 +1,59 @@ -import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; +import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; +import { Component, Inject, OnInit } from "@angular/core"; +import { FormBuilder, Validators } from "@angular/forms"; 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 { DialogService } from "@bitwarden/components"; import { EmergencyAccessService } from "../../emergency-access"; import { EmergencyAccessType } from "../../emergency-access/enums/emergency-access-type"; +export type EmergencyAccessAddEditDialogData = { + /** display name of the account requesting emergency access */ + name: string; + /** traces a unique emergency request */ + emergencyAccessId: string; + /** A boolean indicating whether the emergency access request is in read-only mode (true for view-only, false for editing). */ + readOnly: boolean; +}; + +export enum EmergencyAccessAddEditDialogResult { + Saved = "saved", + Canceled = "canceled", + Deleted = "deleted", +} @Component({ selector: "emergency-access-add-edit", templateUrl: "emergency-access-add-edit.component.html", }) export class EmergencyAccessAddEditComponent implements OnInit { - @Input() name: string; - @Input() emergencyAccessId: string; - @Output() onSaved = new EventEmitter(); - @Output() onDeleted = new EventEmitter(); - loading = true; readOnly = false; editMode = false; title: string; - email: string; type: EmergencyAccessType = EmergencyAccessType.View; - formPromise: Promise; - emergencyAccessType = EmergencyAccessType; waitTimes: { name: string; value: number }[]; - waitTime: number; + addEditForm = this.formBuilder.group({ + email: ["", [Validators.email, Validators.required]], + emergencyAccessType: [this.emergencyAccessType.View], + waitTime: [{ value: null, disabled: this.readOnly }, [Validators.required]], + }); constructor( + @Inject(DIALOG_DATA) protected params: EmergencyAccessAddEditDialogData, + private formBuilder: FormBuilder, private emergencyAccessService: EmergencyAccessService, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private logService: LogService, + private dialogRef: DialogRef, ) {} - async ngOnInit() { - this.editMode = this.loading = this.emergencyAccessId != null; - + this.editMode = this.loading = this.params.emergencyAccessId != null; this.waitTimes = [ { name: this.i18nService.t("oneDay"), value: 1 }, { name: this.i18nService.t("days", "2"), value: 2 }, @@ -50,46 +64,72 @@ export class EmergencyAccessAddEditComponent implements OnInit { ]; if (this.editMode) { - this.editMode = true; this.title = this.i18nService.t("editEmergencyContact"); try { const emergencyAccess = await this.emergencyAccessService.getEmergencyAccess( - this.emergencyAccessId, + this.params.emergencyAccessId, ); - this.type = emergencyAccess.type; - this.waitTime = emergencyAccess.waitTimeDays; + this.addEditForm.patchValue({ + email: emergencyAccess.email, + waitTime: emergencyAccess.waitTimeDays, + emergencyAccessType: emergencyAccess.type, + }); } catch (e) { this.logService.error(e); } } else { this.title = this.i18nService.t("inviteEmergencyContact"); - this.waitTime = this.waitTimes[2].value; + this.addEditForm.patchValue({ waitTime: this.waitTimes[2].value }); } this.loading = false; } - async submit() { + submit = async () => { + if (this.addEditForm.invalid) { + this.addEditForm.markAllAsTouched(); + return; + } try { if (this.editMode) { - await this.emergencyAccessService.update(this.emergencyAccessId, this.type, this.waitTime); + await this.emergencyAccessService.update( + this.params.emergencyAccessId, + this.addEditForm.value.emergencyAccessType, + this.addEditForm.value.waitTime, + ); } else { - await this.emergencyAccessService.invite(this.email, this.type, this.waitTime); + await this.emergencyAccessService.invite( + this.addEditForm.value.email, + this.addEditForm.value.emergencyAccessType, + this.addEditForm.value.waitTime, + ); } - - await this.formPromise; this.platformUtilsService.showToast( "success", null, - this.i18nService.t(this.editMode ? "editedUserId" : "invitedUsers", this.name), + this.i18nService.t(this.editMode ? "editedUserId" : "invitedUsers", this.params.name), ); - this.onSaved.emit(); + this.dialogRef.close(EmergencyAccessAddEditDialogResult.Saved); } catch (e) { this.logService.error(e); } - } + }; - async delete() { - this.onDeleted.emit(); - } + delete = async () => { + this.dialogRef.close(EmergencyAccessAddEditDialogResult.Deleted); + }; + /** + * Strongly typed helper to open a EmergencyAccessAddEditComponent + * @param dialogService Instance of the dialog service that will be used to open the dialog + * @param config Configuration for the dialog + */ + static open = ( + dialogService: DialogService, + config: DialogConfig, + ) => { + return dialogService.open( + EmergencyAccessAddEditComponent, + config, + ); + }; } diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.html b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.html index 27bfc673ec4..41c62db6095 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.html +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.html @@ -1,264 +1,276 @@ - -

- {{ "emergencyAccessDesc" | i18n }} - - {{ "learnMore" | i18n }}. - -

- -

- {{ "warning" | i18n }}: {{ "emergencyAccessOwnerWarning" | i18n }} -

- -