1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-19 09:43:23 +00:00

Merge branch 'main' into autofill/pm-6546-blurring-of-autofilled-elements-causes-problems-in-blur-event-listeners

This commit is contained in:
Cesar Gonzalez
2024-03-11 09:02:47 -05:00
committed by GitHub
58 changed files with 2179 additions and 1619 deletions

View File

@@ -2709,7 +2709,7 @@
"message": "Starte DUO und folge den Schritten, um die Anmeldung zu abzuschließen." "message": "Starte DUO und folge den Schritten, um die Anmeldung zu abzuschließen."
}, },
"duoRequiredForAccount": { "duoRequiredForAccount": {
"message": "Duo two-step login is required for your account." "message": "Für dein Konto ist die Duo Zwei-Faktor-Authentifizierung erforderlich."
}, },
"popoutTheExtensionToCompleteLogin": { "popoutTheExtensionToCompleteLogin": {
"message": "Popout the extension to complete login." "message": "Popout the extension to complete login."

View File

@@ -2005,7 +2005,7 @@
"message": "Seleccione carpeta..." "message": "Seleccione carpeta..."
}, },
"noFoldersFound": { "noFoldersFound": {
"message": "No folders found", "message": "Ninguna carpeta encontrada",
"description": "Used as a message within the notification bar when no folders are found" "description": "Used as a message within the notification bar when no folders are found"
}, },
"orgPermissionsUpdatedMustSetPassword": { "orgPermissionsUpdatedMustSetPassword": {
@@ -2670,25 +2670,25 @@
"message": "Verificar biométricamente" "message": "Verificar biométricamente"
}, },
"awaitingConfirmation": { "awaitingConfirmation": {
"message": "Awaiting confirmation" "message": "Esperando confirmación"
}, },
"couldNotCompleteBiometrics": { "couldNotCompleteBiometrics": {
"message": "Could not complete biometrics." "message": "No se pudo completar la biométrica."
}, },
"needADifferentMethod": { "needADifferentMethod": {
"message": "¿Necesita un método distinto?" "message": "¿Necesita un método distinto?"
}, },
"useMasterPassword": { "useMasterPassword": {
"message": "Use master password" "message": "Usar contraseña maestra"
}, },
"usePin": { "usePin": {
"message": "Usar NIP" "message": "Usar NIP"
}, },
"useBiometrics": { "useBiometrics": {
"message": "Use biometrics" "message": "Usar biométrica"
}, },
"enterVerificationCodeSentToEmail": { "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": { "resendCode": {
"message": "Volver a enviar código" "message": "Volver a enviar código"
@@ -2706,19 +2706,19 @@
} }
}, },
"launchDuoAndFollowStepsToFinishLoggingIn": { "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": { "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": { "popoutTheExtensionToCompleteLogin": {
"message": "Popout the extension to complete login." "message": "Abra la extensión para completar el inicio de sesión."
}, },
"popoutExtension": { "popoutExtension": {
"message": "Popout extension" "message": "Abrir extensión"
}, },
"launchDuo": { "launchDuo": {
"message": "Launch Duo" "message": "Iniciar Duo"
}, },
"importFormatError": { "importFormatError": {
"message": "Los datos no están formateados correctamente. Por favor, comprueba tu archivo de importación e inténtalo de nuevo." "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" "description": "Button text for the setting that allows overriding the default browser autofill settings"
}, },
"saveCipherAttemptSuccess": { "saveCipherAttemptSuccess": {
"message": "Credentials saved successfully!", "message": "¡Credenciales guardadas con éxito!",
"description": "Notification message for when saving credentials has succeeded." "description": "Notification message for when saving credentials has succeeded."
}, },
"updateCipherAttemptSuccess": { "updateCipherAttemptSuccess": {
"message": "Credentials updated successfully!", "message": "¡Credenciales actualizadas con éxito!",
"description": "Notification message for when updating credentials has succeeded." "description": "Notification message for when updating credentials has succeeded."
}, },
"saveCipherAttemptFailed": { "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." "description": "Notification message for when saving credentials has failed."
} }
} }

View File

@@ -92,13 +92,13 @@
"message": "자동 완성" "message": "자동 완성"
}, },
"autoFillLogin": { "autoFillLogin": {
"message": "Auto-fill login" "message": "로그인 자동 완성"
}, },
"autoFillCard": { "autoFillCard": {
"message": "Auto-fill card" "message": "카드 자동 완성"
}, },
"autoFillIdentity": { "autoFillIdentity": {
"message": "Auto-fill identity" "message": "신원 자동 완성"
}, },
"generatePasswordCopied": { "generatePasswordCopied": {
"message": "비밀번호 생성 및 클립보드에 복사" "message": "비밀번호 생성 및 클립보드에 복사"
@@ -110,19 +110,19 @@
"message": "사용할 수 있는 로그인이 없습니다." "message": "사용할 수 있는 로그인이 없습니다."
}, },
"noCards": { "noCards": {
"message": "No cards" "message": "카드 없음"
}, },
"noIdentities": { "noIdentities": {
"message": "No identities" "message": "신원 없음"
}, },
"addLoginMenu": { "addLoginMenu": {
"message": "Add login" "message": "로그인 추가"
}, },
"addCardMenu": { "addCardMenu": {
"message": "Add card" "message": "카드 추가"
}, },
"addIdentityMenu": { "addIdentityMenu": {
"message": "Add identity" "message": "신원 추가"
}, },
"unlockVaultMenu": { "unlockVaultMenu": {
"message": "보관함 잠금 해제" "message": "보관함 잠금 해제"
@@ -220,7 +220,7 @@
"message": "도움말 및 의견" "message": "도움말 및 의견"
}, },
"helpCenter": { "helpCenter": {
"message": "Bitwarden Help center" "message": "Bitwarden 도움말 센터"
}, },
"communityForums": { "communityForums": {
"message": "Explore Bitwarden community forums" "message": "Explore Bitwarden community forums"

View File

@@ -1,4 +1,5 @@
import { PolicyService as AbstractPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; 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 { import {
CachedServices, CachedServices,
@@ -9,11 +10,6 @@ import {
stateProviderFactory, stateProviderFactory,
StateProviderInitOptions, StateProviderInitOptions,
} from "../../../platform/background/service-factories/state-provider.factory"; } 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 { import {
organizationServiceFactory, organizationServiceFactory,
@@ -23,7 +19,6 @@ import {
type PolicyServiceFactoryOptions = FactoryOptions; type PolicyServiceFactoryOptions = FactoryOptions;
export type PolicyServiceInitOptions = PolicyServiceFactoryOptions & export type PolicyServiceInitOptions = PolicyServiceFactoryOptions &
StateServiceInitOptions &
StateProviderInitOptions & StateProviderInitOptions &
OrganizationServiceInitOptions; OrganizationServiceInitOptions;
@@ -36,8 +31,7 @@ export function policyServiceFactory(
"policyService", "policyService",
opts, opts,
async () => async () =>
new BrowserPolicyService( new PolicyService(
await stateServiceFactory(cache, opts),
await stateProviderFactory(cache, opts), await stateProviderFactory(cache, opts),
await organizationServiceFactory(cache, opts), await organizationServiceFactory(cache, opts),
), ),

View File

@@ -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<Policy>) => Object.assign(new Policy(), obj),
initializeAs: "array",
})
protected _policies: BehaviorSubject<Policy[]>;
}

View File

@@ -1,6 +1,8 @@
import { mock, MockProxy } from "jest-mock-extended"; import { mock, MockProxy } from "jest-mock-extended";
import { of } from "rxjs";
import { NOOP_COMMAND_SUFFIX } from "@bitwarden/common/autofill/constants"; 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
@@ -13,6 +15,7 @@ import { MainContextMenuHandler } from "./main-context-menu-handler";
describe("context-menu", () => { describe("context-menu", () => {
let stateService: MockProxy<BrowserStateService>; let stateService: MockProxy<BrowserStateService>;
let autofillSettingsService: MockProxy<AutofillSettingsServiceAbstraction>;
let i18nService: MockProxy<I18nService>; let i18nService: MockProxy<I18nService>;
let logService: MockProxy<LogService>; let logService: MockProxy<LogService>;
@@ -26,6 +29,7 @@ describe("context-menu", () => {
beforeEach(() => { beforeEach(() => {
stateService = mock(); stateService = mock();
autofillSettingsService = mock();
i18nService = mock(); i18nService = mock();
logService = mock(); logService = mock();
@@ -41,14 +45,20 @@ describe("context-menu", () => {
}); });
i18nService.t.mockImplementation((key) => key); 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()); afterEach(() => jest.resetAllMocks());
describe("init", () => { describe("init", () => {
it("has menu disabled", async () => { it("has menu disabled", async () => {
stateService.getDisableContextMenuItem.mockResolvedValue(true); autofillSettingsService.enableContextMenu$ = of(false);
const createdMenu = await sut.init(); const createdMenu = await sut.init();
expect(createdMenu).toBeFalsy(); expect(createdMenu).toBeFalsy();
@@ -56,8 +66,6 @@ describe("context-menu", () => {
}); });
it("has menu enabled, but does not have premium", async () => { it("has menu enabled, but does not have premium", async () => {
stateService.getDisableContextMenuItem.mockResolvedValue(false);
stateService.getCanAccessPremium.mockResolvedValue(false); stateService.getCanAccessPremium.mockResolvedValue(false);
const createdMenu = await sut.init(); const createdMenu = await sut.init();
@@ -66,8 +74,6 @@ describe("context-menu", () => {
}); });
it("has menu enabled and has premium", async () => { it("has menu enabled and has premium", async () => {
stateService.getDisableContextMenuItem.mockResolvedValue(false);
stateService.getCanAccessPremium.mockResolvedValue(true); stateService.getCanAccessPremium.mockResolvedValue(true);
const createdMenu = await sut.init(); const createdMenu = await sut.init();

View File

@@ -1,3 +1,5 @@
import { firstValueFrom } from "rxjs";
import { import {
AUTOFILL_CARD_ID, AUTOFILL_CARD_ID,
AUTOFILL_ID, AUTOFILL_ID,
@@ -14,6 +16,7 @@ import {
ROOT_ID, ROOT_ID,
SEPARATOR_ID, SEPARATOR_ID,
} from "@bitwarden/common/autofill/constants"; } 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; 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 { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; 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 { Account } from "../../models/account";
import { CachedServices } from "../../platform/background/service-factories/factory-options"; import { CachedServices } from "../../platform/background/service-factories/factory-options";
import { import {
@@ -156,6 +160,7 @@ export class MainContextMenuHandler {
constructor( constructor(
private stateService: BrowserStateService, private stateService: BrowserStateService,
private autofillSettingsService: AutofillSettingsServiceAbstraction,
private i18nService: I18nService, private i18nService: I18nService,
private logService: LogService, private logService: LogService,
) {} ) {}
@@ -183,6 +188,7 @@ export class MainContextMenuHandler {
return new MainContextMenuHandler( return new MainContextMenuHandler(
await stateServiceFactory(cachedServices, serviceOptions), await stateServiceFactory(cachedServices, serviceOptions),
await autofillSettingsServiceFactory(cachedServices, serviceOptions),
await i18nServiceFactory(cachedServices, serviceOptions), await i18nServiceFactory(cachedServices, serviceOptions),
await logServiceFactory(cachedServices, serviceOptions), await logServiceFactory(cachedServices, serviceOptions),
); );
@@ -193,8 +199,8 @@ export class MainContextMenuHandler {
* @returns a boolean showing whether or not items were created * @returns a boolean showing whether or not items were created
*/ */
async init(): Promise<boolean> { async init(): Promise<boolean> {
const menuDisabled = await this.stateService.getDisableContextMenuItem(); const menuEnabled = await firstValueFrom(this.autofillSettingsService.enableContextMenu$);
if (menuDisabled) { if (!menuEnabled) {
await MainContextMenuHandler.removeAll(); await MainContextMenuHandler.removeAll();
return false; return false;
} }

View File

@@ -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 { 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 { ProviderService as ProviderServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.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 { ProviderService } from "@bitwarden/common/admin-console/services/provider.service";
import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service";
@@ -175,7 +176,6 @@ import {
} from "@bitwarden/vault-export-core"; } from "@bitwarden/vault-export-core";
import { BrowserOrganizationService } from "../admin-console/services/browser-organization.service"; 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 ContextMenusBackground from "../autofill/background/context-menus.background";
import NotificationBackground from "../autofill/background/notification.background"; import NotificationBackground from "../autofill/background/notification.background";
import OverlayBackground from "../autofill/background/overlay.background"; import OverlayBackground from "../autofill/background/overlay.background";
@@ -501,11 +501,7 @@ export default class MainBackground {
this.stateService, this.stateService,
this.stateProvider, this.stateProvider,
); );
this.policyService = new BrowserPolicyService( this.policyService = new PolicyService(this.stateProvider, this.organizationService);
this.stateService,
this.stateProvider,
this.organizationService,
);
this.autofillSettingsService = new AutofillSettingsService( this.autofillSettingsService = new AutofillSettingsService(
this.stateProvider, this.stateProvider,
this.policyService, this.policyService,
@@ -818,6 +814,7 @@ export default class MainBackground {
this.stateService, this.stateService,
this.autofillSettingsService, this.autofillSettingsService,
this.vaultTimeoutSettingsService, this.vaultTimeoutSettingsService,
this.biometricStateService,
); );
// Other fields // Other fields
@@ -953,6 +950,7 @@ export default class MainBackground {
if (!this.popupOnlyContext) { if (!this.popupOnlyContext) {
this.mainContextMenuHandler = new MainContextMenuHandler( this.mainContextMenuHandler = new MainContextMenuHandler(
this.stateService, this.stateService,
this.autofillSettingsService,
this.i18nService, this.i18nService,
this.logService, this.logService,
); );

View File

@@ -84,10 +84,6 @@ export class NativeMessagingBackground {
private authService: AuthService, private authService: AuthService,
private biometricStateService: BiometricStateService, 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) { if (chrome?.permissions?.onAdded) {
// Reload extension to activate nativeMessaging // Reload extension to activate nativeMessaging
chrome.permissions.onAdded.addListener((permissions) => { chrome.permissions.onAdded.addListener((permissions) => {
@@ -100,9 +96,7 @@ export class NativeMessagingBackground {
async connect() { async connect() {
this.appId = await this.appIdService.getAppId(); 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. await this.biometricStateService.setFingerprintValidated(false);
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.stateService.setBiometricFingerprintValidated(false);
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
this.port = BrowserApi.connectNative("com.8bit.bitwarden"); this.port = BrowserApi.connectNative("com.8bit.bitwarden");
@@ -148,9 +142,7 @@ export class NativeMessagingBackground {
if (this.validatingFingerprint) { if (this.validatingFingerprint) {
this.validatingFingerprint = false; 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. await this.biometricStateService.setFingerprintValidated(true);
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.stateService.setBiometricFingerprintValidated(true);
} }
this.sharedSecret = new SymmetricCryptoKey(decrypted); this.sharedSecret = new SymmetricCryptoKey(decrypted);
this.secureSetupResolve(); this.secureSetupResolve();

View File

@@ -102,7 +102,6 @@ import { ImportServiceAbstraction } from "@bitwarden/importer/core";
import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core"; import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core";
import { BrowserOrganizationService } from "../../admin-console/services/browser-organization.service"; 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 { UnauthGuardService } from "../../auth/popup/services";
import { AutofillService } from "../../autofill/services/abstractions/autofill.service"; import { AutofillService } from "../../autofill/services/abstractions/autofill.service";
import MainBackground from "../../background/main.background"; import MainBackground from "../../background/main.background";
@@ -238,8 +237,9 @@ function getBgService<T>(service: keyof MainBackground) {
}, },
{ {
provide: LogServiceAbstraction, provide: LogServiceAbstraction,
useFactory: getBgService<ConsoleLogService>("logService"), useFactory: (platformUtilsService: PlatformUtilsService) =>
deps: [], new ConsoleLogService(platformUtilsService.isDev()),
deps: [PlatformUtilsService],
}, },
{ {
provide: BrowserEnvironmentService, provide: BrowserEnvironmentService,
@@ -293,17 +293,6 @@ function getBgService<T>(service: keyof MainBackground) {
useFactory: getBgService<EventCollectionService>("eventCollectionService"), useFactory: getBgService<EventCollectionService>("eventCollectionService"),
deps: [], deps: [],
}, },
{
provide: PolicyService,
useFactory: (
stateService: StateServiceAbstraction,
stateProvider: StateProvider,
organizationService: OrganizationService,
) => {
return new BrowserPolicyService(stateService, stateProvider, organizationService);
},
deps: [StateServiceAbstraction, StateProvider, OrganizationService],
},
{ {
provide: PlatformUtilsService, provide: PlatformUtilsService,
useFactory: getBgService<PlatformUtilsService>("platformUtilsService"), useFactory: getBgService<PlatformUtilsService>("platformUtilsService"),

View File

@@ -105,7 +105,9 @@ export class OptionsComponent implements OnInit {
this.userNotificationSettingsService.enableChangedPasswordPrompt$, this.userNotificationSettingsService.enableChangedPasswordPrompt$,
); );
this.enableContextMenuItem = !(await this.stateService.getDisableContextMenuItem()); this.enableContextMenuItem = await firstValueFrom(
this.autofillSettingsService.enableContextMenu$,
);
this.showCardsCurrentTab = !(await this.stateService.getDontShowCardsCurrentTab()); this.showCardsCurrentTab = !(await this.stateService.getDontShowCardsCurrentTab());
this.showIdentitiesCurrentTab = !(await this.stateService.getDontShowIdentitiesCurrentTab()); this.showIdentitiesCurrentTab = !(await this.stateService.getDontShowIdentitiesCurrentTab());
@@ -143,7 +145,7 @@ export class OptionsComponent implements OnInit {
} }
async updateContextMenuItem() { async updateContextMenuItem() {
await this.stateService.setDisableContextMenuItem(!this.enableContextMenuItem); await this.autofillSettingsService.setEnableContextMenu(this.enableContextMenuItem);
this.messagingService.send("bgUpdateContextMenu"); this.messagingService.send("bgUpdateContextMenu");
} }

View File

@@ -415,7 +415,7 @@ export class SettingsComponent implements OnInit {
]); ]);
} else { } else {
await this.biometricStateService.setBiometricUnlockEnabled(false); await this.biometricStateService.setBiometricUnlockEnabled(false);
await this.stateService.setBiometricFingerprintValidated(false); await this.biometricStateService.setFingerprintValidated(false);
} }
} }

View File

@@ -71,7 +71,7 @@
"papaparse": "5.4.1", "papaparse": "5.4.1",
"proper-lockfile": "4.1.2", "proper-lockfile": "4.1.2",
"rxjs": "7.8.1", "rxjs": "7.8.1",
"tldts": "6.1.11", "tldts": "6.1.13",
"zxcvbn": "4.4.2" "zxcvbn": "4.4.2"
} }
} }

View File

@@ -394,11 +394,7 @@ export class Main {
this.organizationUserService = new OrganizationUserServiceImplementation(this.apiService); this.organizationUserService = new OrganizationUserServiceImplementation(this.apiService);
this.policyService = new PolicyService( this.policyService = new PolicyService(this.stateProvider, this.organizationService);
this.stateService,
this.stateProvider,
this.organizationService,
);
this.policyApiService = new PolicyApiService(this.policyService, this.apiService); this.policyApiService = new PolicyApiService(this.policyService, this.apiService);
@@ -653,7 +649,7 @@ export class Main {
this.cipherService.clear(userId), this.cipherService.clear(userId),
this.folderService.clear(userId), this.folderService.clear(userId),
this.collectionService.clear(userId as UserId), this.collectionService.clear(userId as UserId),
this.policyService.clear(userId), this.policyService.clear(userId as UserId),
this.passwordGenerationService.clear(), this.passwordGenerationService.clear(),
this.providerService.save(null, userId as UserId), this.providerService.save(null, userId as UserId),
]); ]);

View File

@@ -510,9 +510,9 @@ checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
[[package]] [[package]]
name = "gio" name = "gio"
version = "0.18.4" version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" checksum = "2eae10b27b6dd27e22ed0d812c6387deba295e6fc004a8b379e459b663b05a02"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
@@ -521,7 +521,6 @@ dependencies = [
"gio-sys", "gio-sys",
"glib", "glib",
"libc", "libc",
"once_cell",
"pin-project-lite", "pin-project-lite",
"smallvec", "smallvec",
"thiserror", "thiserror",
@@ -529,22 +528,22 @@ dependencies = [
[[package]] [[package]]
name = "gio-sys" name = "gio-sys"
version = "0.18.1" version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" checksum = "bcf8e1d9219bb294636753d307b030c1e8a032062cba74f493c431a5c8b81ce4"
dependencies = [ dependencies = [
"glib-sys", "glib-sys",
"gobject-sys", "gobject-sys",
"libc", "libc",
"system-deps", "system-deps",
"winapi", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
name = "glib" name = "glib"
version = "0.18.2" version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c316afb01ce8067c5eaab1fc4f2cd47dc21ce7b6296358605e2ffab23ccbd19" checksum = "ab9e86540b5d8402e905ad4ce7d6aa544092131ab564f3102175af176b90a053"
dependencies = [ dependencies = [
"bitflags 2.4.1", "bitflags 2.4.1",
"futures-channel", "futures-channel",
@@ -558,20 +557,18 @@ dependencies = [
"gobject-sys", "gobject-sys",
"libc", "libc",
"memchr", "memchr",
"once_cell",
"smallvec", "smallvec",
"thiserror", "thiserror",
] ]
[[package]] [[package]]
name = "glib-macros" name = "glib-macros"
version = "0.18.2" version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8da903822b136d42360518653fcf154455defc437d3e7a81475bf9a95ff1e47" checksum = "0f5897ca27a83e4cdc7b4666850bade0a2e73e17689aabafcc9acddad9d823b8"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro-crate", "proc-macro-crate",
"proc-macro-error",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.38", "syn 2.0.38",
@@ -579,9 +576,9 @@ dependencies = [
[[package]] [[package]]
name = "glib-sys" name = "glib-sys"
version = "0.18.1" version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" checksum = "630f097773d7c7a0bb3258df4e8157b47dc98bbfa0e60ad9ab56174813feced4"
dependencies = [ dependencies = [
"libc", "libc",
"system-deps", "system-deps",
@@ -589,9 +586,9 @@ dependencies = [
[[package]] [[package]]
name = "gobject-sys" name = "gobject-sys"
version = "0.18.0" version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" checksum = "c85e2b1080b9418dd0c58b498da3a5c826030343e0ef07bde6a955d28de54979"
dependencies = [ dependencies = [
"glib-sys", "glib-sys",
"libc", "libc",
@@ -681,9 +678,9 @@ dependencies = [
[[package]] [[package]]
name = "libsecret" name = "libsecret"
version = "0.4.0" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac6fae6ebe590e06ef9d01b125e46b7d4c05ccbd5961f12b4aefe2ecd010220f" checksum = "50c6ccddc706a38eca477b4d7857acd6c76c7d6fba5d47b4b2e7d800e5a17194"
dependencies = [ dependencies = [
"gio", "gio",
"glib", "glib",
@@ -693,9 +690,9 @@ dependencies = [
[[package]] [[package]]
name = "libsecret-sys" name = "libsecret-sys"
version = "0.4.0" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b716fc5e1c82eb0d28665882628382ab0e0a156a6d73580e33f0ac6ac8d2540" checksum = "3a1af48e61f1c8e77e9705296f346e45b637754a92348a79b4c62df84d0654c2"
dependencies = [ dependencies = [
"gio-sys", "gio-sys",
"glib-sys", "glib-sys",
@@ -747,9 +744,9 @@ dependencies = [
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.6.4" version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
[[package]] [[package]]
name = "memoffset" name = "memoffset"
@@ -990,36 +987,11 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]] [[package]]
name = "proc-macro-crate" name = "proc-macro-crate"
version = "1.3.1" version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284"
dependencies = [ dependencies = [
"once_cell", "toml_edit 0.21.1",
"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",
] ]
[[package]] [[package]]
@@ -1252,9 +1224,9 @@ dependencies = [
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.11.1" version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
[[package]] [[package]]
name = "socket2" name = "socket2"
@@ -1406,17 +1378,6 @@ dependencies = [
"serde", "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]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.20.7" version = "0.20.7"
@@ -1430,6 +1391,17 @@ dependencies = [
"winnow", "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]] [[package]]
name = "tree_magic_mini" name = "tree_magic_mini"
version = "3.0.3" version = "3.0.3"

View File

@@ -54,5 +54,5 @@ security-framework = "=2.9.2"
security-framework-sys = "=2.9.1" security-framework-sys = "=2.9.1"
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
gio = "=0.18.4" gio = "=0.19.2"
libsecret = "=0.4.0" libsecret = "=0.5.0"

View File

@@ -126,6 +126,7 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
StateServiceAbstraction, StateServiceAbstraction,
AutofillSettingsServiceAbstraction, AutofillSettingsServiceAbstraction,
VaultTimeoutSettingsService, VaultTimeoutSettingsService,
BiometricStateService,
], ],
}, },
{ {

View File

@@ -28,7 +28,7 @@
<button type="submit" class="btn primary block" [disabled]="!accessibilityForm.valid"> <button type="submit" class="btn primary block" [disabled]="!accessibilityForm.valid">
{{ "submit" | i18n }} {{ "submit" | i18n }}
</button> </button>
<button type="button" routerLink="/login" class="btn block">{{ "done" | i18n }}</button> <button type="button" (click)="close()" class="btn block">{{ "done" | i18n }}</button>
</div> </div>
</div> </div>
</form> </form>

View File

@@ -2,14 +2,11 @@ import { Component, NgZone } from "@angular/core";
import { UntypedFormControl, UntypedFormGroup, Validators } from "@angular/forms"; import { UntypedFormControl, UntypedFormGroup, Validators } from "@angular/forms";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
const BroadcasterSubscriptionId = "AccessibilityCookieComponent";
@Component({ @Component({
selector: "app-accessibility-cookie", selector: "app-accessibility-cookie",
templateUrl: "accessibility-cookie.component.html", templateUrl: "accessibility-cookie.component.html",
@@ -27,40 +24,21 @@ export class AccessibilityCookieComponent {
protected platformUtilsService: PlatformUtilsService, protected platformUtilsService: PlatformUtilsService,
protected environmentService: EnvironmentService, protected environmentService: EnvironmentService,
protected i18nService: I18nService, protected i18nService: I18nService,
private broadcasterService: BroadcasterService,
protected ngZone: NgZone, 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() { registerhCaptcha() {
this.platformUtilsService.launchUri("https://www.hcaptcha.com/accessibility"); this.platformUtilsService.launchUri("https://www.hcaptcha.com/accessibility");
} }
async checkForCookie() { async close() {
this.hCaptchaWindow.close();
const [cookie] = await ipc.auth.getHcaptchaAccessibilityCookie(); const [cookie] = await ipc.auth.getHcaptchaAccessibilityCookie();
if (cookie) { if (cookie) {
this.onCookieSavedSuccess(); this.onCookieSavedSuccess();
} else { } else {
this.onCookieSavedFailure(); this.onCookieSavedFailure();
} }
await this.router.navigate(["/login"]);
} }
onCookieSavedSuccess() { onCookieSavedSuccess() {
@@ -89,10 +67,6 @@ export class AccessibilityCookieComponent {
return; return;
} }
this.listenForCookie = true; this.listenForCookie = true;
this.hCaptchaWindow = window.open(this.accessibilityForm.value.link); window.open(this.accessibilityForm.value.link, "_blank", "noopener noreferrer");
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
} }
} }

View File

@@ -30,7 +30,7 @@ export class WindowMain {
private windowStateChangeTimer: NodeJS.Timeout; private windowStateChangeTimer: NodeJS.Timeout;
private windowStates: { [key: string]: WindowState } = {}; private windowStates: { [key: string]: WindowState } = {};
private enableAlwaysOnTop = false; private enableAlwaysOnTop = false;
private session: Electron.Session; session: Electron.Session;
readonly defaultWidth = 950; readonly defaultWidth = 950;
readonly defaultHeight = 600; readonly defaultHeight = 600;

View File

@@ -1,16 +1,6 @@
import * as path from "path"; import * as path from "path";
import { import { app, dialog, ipcMain, Menu, MenuItem, nativeTheme, Notification, shell } from "electron";
app,
dialog,
ipcMain,
Menu,
MenuItem,
nativeTheme,
session,
Notification,
shell,
} from "electron";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { ThemeType } from "@bitwarden/common/platform/enums"; import { ThemeType } from "@bitwarden/common/platform/enums";
@@ -64,7 +54,7 @@ export class ElectronMainMessagingService implements MessagingService {
}); });
ipcMain.handle("getCookie", async (event, options) => { 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) => { ipcMain.handle("loginRequest", async (event, options) => {

View File

@@ -1,52 +1,37 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="confirmUserTitle"> <form [formGroup]="confirmForm" [bitSubmit]="submit">
<div class="modal-dialog modal-dialog-scrollable" role="document"> <bit-dialog dialogSize="large" [loading]="loading">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise"> <span bitDialogTitle>
<div class="modal-header"> {{ "confirmUser" | i18n }}
<h1 class="modal-title" id="confirmUserTitle"> <small class="tw-text-muted">{{ params.name }}</small>
{{ "confirmUser" | i18n }} </span>
<small class="text-muted" *ngIf="name">{{ name }}</small> <div bitDialogContent>
</h1> <p bitTypography="body1">
<button {{ "fingerprintEnsureIntegrityVerify" | i18n }}
type="button" <a
class="close" bitLink
data-dismiss="modal" href="https://bitwarden.com/help/fingerprint-phrase/"
appA11yTitle="{{ 'close' | i18n }}" target="_blank"
rel="noreferrer"
> >
<span aria-hidden="true">&times;</span> {{ "learnMore" | i18n }}</a
</button> >
</div> </p>
<div class="modal-body"> <p bitTypography="body1">
<p> <code>{{ fingerprint }}</code>
{{ "fingerprintEnsureIntegrityVerify" | i18n }} </p>
<a href="https://bitwarden.com/help/fingerprint-phrase/" target="_blank" rel="noreferrer">
{{ "learnMore" | i18n }}</a <bit-form-control>
> <input type="checkbox" bitCheckbox formControlName="dontAskAgain" />
</p> <bit-label> {{ "dontAskFingerprintAgain" | i18n }}</bit-label>
<p> </bit-form-control>
<code>{{ fingerprint }}</code> </div>
</p> <div bitDialogFooter>
<div class="form-check"> <button type="submit" buttonType="primary" bitButton bitFormButton>
<input <span>{{ "confirm" | i18n }}</span>
class="form-check-input" </button>
type="checkbox" <button bitButton bitFormButton buttonType="secondary" type="button" bitDialogClose>
id="dontAskAgain" {{ "cancel" | i18n }}
name="DontAskAgain" </button>
[(ngModel)]="dontAskAgain" </div>
/> </bit-dialog>
<label class="form-check-label" for="dontAskAgain"> </form>
{{ "dontAskFingerprintAgain" | i18n }}
</label>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "confirm" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "cancel" | i18n }}
</button>
</div>
</form>
</div>
</div>

View File

@@ -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 { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; 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({ @Component({
selector: "emergency-access-confirm", selector: "emergency-access-confirm",
templateUrl: "emergency-access-confirm.component.html", templateUrl: "emergency-access-confirm.component.html",
}) })
export class EmergencyAccessConfirmComponent implements OnInit { export class EmergencyAccessConfirmComponent implements OnInit {
@Input() name: string;
@Input() userId: string;
@Input() emergencyAccessId: string;
@Input() formPromise: Promise<any>;
@Output() onConfirmed = new EventEmitter();
dontAskAgain = false;
loading = true; loading = true;
fingerprint: string; fingerprint: string;
confirmForm = this.formBuilder.group({
dontAskAgain: [false],
});
constructor( constructor(
@Inject(DIALOG_DATA) protected params: EmergencyAccessConfirmDialogData,
private formBuilder: FormBuilder,
private apiService: ApiService, private apiService: ApiService,
private cryptoService: CryptoService, private cryptoService: CryptoService,
private stateService: StateService, private stateService: StateService,
private logService: LogService, private logService: LogService,
private dialogRef: DialogRef<EmergencyAccessConfirmDialogResult>,
) {} ) {}
async ngOnInit() { async ngOnInit() {
try { try {
const publicKeyResponse = await this.apiService.getUserPublicKey(this.userId); const publicKeyResponse = await this.apiService.getUserPublicKey(this.params.userId);
if (publicKeyResponse != null) { if (publicKeyResponse != null) {
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey); 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) { if (fingerprint != null) {
this.fingerprint = fingerprint.join("-"); this.fingerprint = fingerprint.join("-");
} }
@@ -44,19 +57,33 @@ export class EmergencyAccessConfirmComponent implements OnInit {
this.loading = false; this.loading = false;
} }
async submit() { submit = async () => {
if (this.loading) { if (this.loading) {
return; return;
} }
if (this.dontAskAgain) { if (this.confirmForm.get("dontAskAgain").value) {
await this.stateService.setAutoConfirmFingerprints(true); await this.stateService.setAutoConfirmFingerprints(true);
} }
try { try {
this.onConfirmed.emit(); this.dialogRef.close(EmergencyAccessConfirmDialogResult.Confirmed);
} catch (e) { } catch (e) {
this.logService.error(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<EmergencyAccessConfirmDialogData>,
) {
return dialogService.open<EmergencyAccessConfirmDialogResult>(
EmergencyAccessConfirmComponent,
config,
);
} }
} }

View File

@@ -1,142 +1,68 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="userAddEditTitle"> <form [formGroup]="addEditForm" [bitSubmit]="submit">
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document"> <bit-dialog dialogSize="large" [loading]="loading">
<form <span bitDialogTitle>
class="modal-content" <app-premium-badge *ngIf="readOnly"></app-premium-badge>
#form {{ title }}
(ngSubmit)="submit()" <small class="tw-text-muted" *ngIf="params.name">{{ params.name }}</small>
[appApiAction]="formPromise" </span>
ngNativeValidate <ng-container bitDialogContent>
> <ng-container *ngIf="!editMode">
<div class="modal-header"> <p bitTypography="body1">{{ "inviteEmergencyContactDesc" | i18n }}</p>
<h1 class="modal-title" id="userAddEditTitle"> <bit-form-field>
<app-premium-badge *ngIf="readOnly"></app-premium-badge> <bit-label>{{ "email" | i18n }}</bit-label>
{{ title }} <input bitInput formControlName="email" />
<small class="text-muted" *ngIf="name">{{ name }}</small> </bit-form-field>
</h1> </ng-container>
<button <bit-radio-group formControlName="emergencyAccessType" [block]="true">
type="button" <bit-label>
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body" *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</div>
<div class="modal-body" *ngIf="!loading">
<ng-container *ngIf="!editMode">
<p>{{ "inviteEmergencyContactDesc" | i18n }}</p>
<div class="form-group mb-4">
<label for="email">{{ "email" | i18n }}</label>
<input
id="email"
class="form-control"
type="text"
name="Email"
[(ngModel)]="email"
required
/>
</div>
</ng-container>
<h3>
{{ "userAccess" | i18n }} {{ "userAccess" | i18n }}
<a <a
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
bitLink
linkType="primary"
appA11yTitle="{{ 'learnMore' | i18n }}" appA11yTitle="{{ 'learnMore' | i18n }}"
href="https://bitwarden.com/help/emergency-access/#user-access" href="https://bitwarden.com/help/emergency-access/#user-access"
> >
<i class="bwi bwi-question-circle" aria-hidden="true"></i> <i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a> </a>
</h3> </bit-label>
<div class="form-check mt-2 form-check-block"> <bit-radio-button id="emergencyTypeView" [value]="emergencyAccessType.View">
<input <bit-label>{{ "view" | i18n }}</bit-label>
class="form-check-input" <bit-hint>{{ "viewDesc" | i18n }}</bit-hint>
type="radio" </bit-radio-button>
name="userType"
id="emergencyTypeView" <bit-radio-button id="emergencyTypeTakeover" [value]="emergencyAccessType.Takeover">
[value]="emergencyAccessType.View" <bit-label>{{ "takeover" | i18n }}</bit-label>
[(ngModel)]="type" <bit-hint>{{ "takeoverDesc" | i18n }}</bit-hint>
/> </bit-radio-button>
<label class="form-check-label" for="emergencyTypeView"> </bit-radio-group>
{{ "view" | i18n }}
<small>{{ "viewDesc" | i18n }}</small> <bit-form-field class="tw-w-1/2 tw-relative tw-px-2.5">
</label> <bit-label>{{ "waitTime" | i18n }}</bit-label>
</div> <bit-select formControlName="waitTime">
<div class="form-check mt-2 form-check-block"> <bit-option *ngFor="let o of waitTimes" [value]="o.value" [label]="o.name"></bit-option>
<input </bit-select>
class="form-check-input" <bit-hint class="tw-text-sm">{{ "waitTimeDesc" | i18n }}</bit-hint>
type="radio" </bit-form-field>
name="userType" </ng-container>
id="emergencyTypeTakeover" <ng-container bitDialogFooter>
[value]="emergencyAccessType.Takeover" <button type="submit" buttonType="primary" bitButton bitFormButton [disabled]="readOnly">
[(ngModel)]="type" {{ "save" | i18n }}
[disabled]="readOnly" </button>
/> <button bitButton bitFormButton buttonType="secondary" type="button" bitDialogClose>
<label class="form-check-label" for="emergencyTypeTakeover"> {{ "cancel" | i18n }}
{{ "takeover" | i18n }} </button>
<small>{{ "takeoverDesc" | i18n }}</small> <button
</label> type="button"
</div> bitFormButton
<div class="form-group col-6 mt-4"> class="tw-ml-auto"
<label for="waitTime">{{ "waitTime" | i18n }}</label> bitIconButton="bwi-trash"
<select buttonType="danger"
id="waitTime" [bitAction]="delete"
name="waitTime" *ngIf="editMode"
[(ngModel)]="waitTime" appA11yTitle="{{ 'delete' | i18n }}"
class="form-control" ></button>
[disabled]="readOnly" </ng-container>
> </bit-dialog>
<option *ngFor="let o of waitTimes" [ngValue]="o.value">{{ o.name }}</option> </form>
</select>
<small class="text-muted">{{ "waitTimeDesc" | i18n }}</small>
</div>
</div>
<div class="modal-footer">
<button
type="submit"
buttonType="primary"
bitButton
[loading]="loading || form.loading"
[disabled]="readOnly"
>
{{ "save" | i18n }}
</button>
<button bitButton buttonType="secondary" type="button" data-dismiss="modal">
{{ "cancel" | i18n }}
</button>
<div class="ml-auto">
<button
#deleteBtn
bitButton
buttonType="danger"
type="button"
(click)="delete()"
appA11yTitle="{{ 'delete' | i18n }}"
*ngIf="editMode"
[disabled]="$any(deleteBtn).loading"
>
<i
class="bwi bwi-trash bwi-lg bwi-fw"
[hidden]="$any(deleteBtn).loading"
aria-hidden="true"
></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!$any(deleteBtn).loading"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
</button>
</div>
</div>
</form>
</div>
</div>

View File

@@ -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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService } from "@bitwarden/components";
import { EmergencyAccessService } from "../../emergency-access"; import { EmergencyAccessService } from "../../emergency-access";
import { EmergencyAccessType } from "../../emergency-access/enums/emergency-access-type"; 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({ @Component({
selector: "emergency-access-add-edit", selector: "emergency-access-add-edit",
templateUrl: "emergency-access-add-edit.component.html", templateUrl: "emergency-access-add-edit.component.html",
}) })
export class EmergencyAccessAddEditComponent implements OnInit { export class EmergencyAccessAddEditComponent implements OnInit {
@Input() name: string;
@Input() emergencyAccessId: string;
@Output() onSaved = new EventEmitter();
@Output() onDeleted = new EventEmitter();
loading = true; loading = true;
readOnly = false; readOnly = false;
editMode = false; editMode = false;
title: string; title: string;
email: string;
type: EmergencyAccessType = EmergencyAccessType.View; type: EmergencyAccessType = EmergencyAccessType.View;
formPromise: Promise<any>;
emergencyAccessType = EmergencyAccessType; emergencyAccessType = EmergencyAccessType;
waitTimes: { name: string; value: number }[]; 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( constructor(
@Inject(DIALOG_DATA) protected params: EmergencyAccessAddEditDialogData,
private formBuilder: FormBuilder,
private emergencyAccessService: EmergencyAccessService, private emergencyAccessService: EmergencyAccessService,
private i18nService: I18nService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private logService: LogService, private logService: LogService,
private dialogRef: DialogRef<EmergencyAccessAddEditDialogResult>,
) {} ) {}
async ngOnInit() { async ngOnInit() {
this.editMode = this.loading = this.emergencyAccessId != null; this.editMode = this.loading = this.params.emergencyAccessId != null;
this.waitTimes = [ this.waitTimes = [
{ name: this.i18nService.t("oneDay"), value: 1 }, { name: this.i18nService.t("oneDay"), value: 1 },
{ name: this.i18nService.t("days", "2"), value: 2 }, { name: this.i18nService.t("days", "2"), value: 2 },
@@ -50,46 +64,72 @@ export class EmergencyAccessAddEditComponent implements OnInit {
]; ];
if (this.editMode) { if (this.editMode) {
this.editMode = true;
this.title = this.i18nService.t("editEmergencyContact"); this.title = this.i18nService.t("editEmergencyContact");
try { try {
const emergencyAccess = await this.emergencyAccessService.getEmergencyAccess( const emergencyAccess = await this.emergencyAccessService.getEmergencyAccess(
this.emergencyAccessId, this.params.emergencyAccessId,
); );
this.type = emergencyAccess.type; this.addEditForm.patchValue({
this.waitTime = emergencyAccess.waitTimeDays; email: emergencyAccess.email,
waitTime: emergencyAccess.waitTimeDays,
emergencyAccessType: emergencyAccess.type,
});
} catch (e) { } catch (e) {
this.logService.error(e); this.logService.error(e);
} }
} else { } else {
this.title = this.i18nService.t("inviteEmergencyContact"); this.title = this.i18nService.t("inviteEmergencyContact");
this.waitTime = this.waitTimes[2].value; this.addEditForm.patchValue({ waitTime: this.waitTimes[2].value });
} }
this.loading = false; this.loading = false;
} }
async submit() { submit = async () => {
if (this.addEditForm.invalid) {
this.addEditForm.markAllAsTouched();
return;
}
try { try {
if (this.editMode) { 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 { } 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( this.platformUtilsService.showToast(
"success", "success",
null, 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) { } catch (e) {
this.logService.error(e); this.logService.error(e);
} }
} };
async delete() { delete = async () => {
this.onDeleted.emit(); 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<EmergencyAccessAddEditDialogData>,
) => {
return dialogService.open<EmergencyAccessAddEditDialogResult>(
EmergencyAccessAddEditComponent,
config,
);
};
} }

View File

@@ -1,264 +1,276 @@
<app-header></app-header> <app-header></app-header>
<bit-container> <bit-container>
<p> <bit-section>
{{ "emergencyAccessDesc" | i18n }} <p bitTypography="body1">
<a href="https://bitwarden.com/help/emergency-access/" target="_blank" rel="noreferrer"> <span class="tw-text-main">{{ "emergencyAccessDesc" | i18n }}</span>
{{ "learnMore" | i18n }}. <a
</a> bitLink
</p> href="https://bitwarden.com/help/emergency-access/"
target="_blank"
<p *ngIf="isOrganizationOwner"> rel="noreferrer"
<b>{{ "warning" | i18n }}:</b> {{ "emergencyAccessOwnerWarning" | i18n }}
</p>
<div class="page-header d-flex">
<h2>
{{ "trustedEmergencyContacts" | i18n }}
<app-premium-badge></app-premium-badge>
</h2>
<div class="ml-auto d-flex">
<button
class="btn btn-sm btn-outline-primary ml-3"
type="button"
(click)="invite()"
[disabled]="!canAccessPremium"
> >
<i aria-hidden="true" class="bwi bwi-plus bwi-fw"></i> {{ "learnMore" | i18n }}.
{{ "addEmergencyContact" | i18n }} </a>
</button> </p>
<bit-callout *ngIf="isOrganizationOwner" type="warning" title="{{ 'warning' | i18n }}">{{
"emergencyAccessOwnerWarning" | i18n
}}</bit-callout>
</bit-section>
<bit-section>
<div class="tw-flex tw-items-center tw-gap-2 tw-mb-2">
<h2 bitTypography="h2" noMargin class="tw-mb-0">
{{ "trustedEmergencyContacts" | i18n }}
</h2>
<app-premium-badge></app-premium-badge>
<div class="tw-ml-auto tw-flex">
<button
type="button"
bitButton
buttonType="primary"
[bitAction]="invite"
[disabled]="!canAccessPremium"
>
<i aria-hidden="true" class="bwi bwi-plus bwi-fw"></i>
{{ "addEmergencyContact" | i18n }}
</button>
</div>
</div> </div>
</div> <bit-table *ngIf="trustedContacts && trustedContacts.length">
<ng-container header>
<tr>
<th bitCell>{{ "name" | i18n }}</th>
<th bitCell>{{ "accessLevel" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "options" | i18n }}</th>
</tr>
</ng-container>
<ng-template body>
<tr bitRow *ngFor="let c of trustedContacts; let i = index">
<td bitCell class="tw-flex tw-items-center tw-gap-4">
<bit-avatar
[text]="c | userName"
[id]="c.granteeId"
[color]="c.avatarColor"
size="small"
></bit-avatar>
<span>
<a bitLink href="#" appStopClick (click)="edit(c)">{{ c.email }}</a>
<span
bitBadge
variant="secondary"
*ngIf="c.status === emergencyAccessStatusType.Invited"
>{{ "invited" | i18n }}</span
>
<span
bitBadge
variant="warning"
*ngIf="c.status === emergencyAccessStatusType.Accepted"
>{{ "accepted" | i18n }}</span
>
<span
bitBadge
variant="warning"
*ngIf="c.status === emergencyAccessStatusType.RecoveryInitiated"
>{{ "emergencyAccessRecoveryInitiated" | i18n }}</span
>
<span bitBadge *ngIf="c.status === emergencyAccessStatusType.RecoveryApproved">{{
"emergencyAccessRecoveryApproved" | i18n
}}</span>
<table <small class="tw-text-muted tw-block" *ngIf="c.name">{{ c.name }}</small>
class="table table-hover table-list mb-0" </span>
*ngIf="trustedContacts && trustedContacts.length" </td>
> <td bitCell>
<tbody> <span bitBadge *ngIf="c.type === emergencyAccessType.View">{{ "view" | i18n }}</span>
<tr *ngFor="let c of trustedContacts; let i = index"> <span bitBadge *ngIf="c.type === emergencyAccessType.Takeover">{{
<td width="30"> "takeover" | i18n
<bit-avatar }}</span>
[text]="c | userName" </td>
[id]="c.granteeId" <td bitCell class="tw-text-right">
[color]="c.avatarColor"
size="small"
></bit-avatar>
</td>
<td>
<a href="#" appStopClick (click)="edit(c)">{{ c.email }}</a>
<span
bitBadge
variant="secondary"
*ngIf="c.status === emergencyAccessStatusType.Invited"
>{{ "invited" | i18n }}</span
>
<span
bitBadge
variant="warning"
*ngIf="c.status === emergencyAccessStatusType.Accepted"
>{{ "accepted" | i18n }}</span
>
<span
bitBadge
variant="warning"
*ngIf="c.status === emergencyAccessStatusType.RecoveryInitiated"
>{{ "emergencyAccessRecoveryInitiated" | i18n }}</span
>
<span bitBadge *ngIf="c.status === emergencyAccessStatusType.RecoveryApproved">{{
"emergencyAccessRecoveryApproved" | i18n
}}</span>
<span bitBadge *ngIf="c.type === emergencyAccessType.View">{{ "view" | i18n }}</span>
<span bitBadge *ngIf="c.type === emergencyAccessType.Takeover">{{
"takeover" | i18n
}}</span>
<small class="text-muted d-block" *ngIf="c.name">{{ c.name }}</small>
</td>
<td class="table-list-options">
<button
[bitMenuTriggerFor]="trustedContactOptions"
class="tw-border-none tw-bg-transparent tw-text-main"
type="button"
appA11yTitle="{{ 'options' | i18n }}"
>
<i class="bwi bwi-ellipsis-v bwi-lg" aria-hidden="true"></i>
</button>
<bit-menu #trustedContactOptions>
<button <button
[bitMenuTriggerFor]="trustedContactOptions"
type="button" type="button"
bitMenuItem appA11yTitle="{{ 'options' | i18n }}"
*ngIf="c.status === emergencyAccessStatusType.Invited" bitIconButton="bwi-ellipsis-v"
(click)="reinvite(c)" buttonType="main"
> ></button>
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i> <bit-menu #trustedContactOptions>
{{ "resendInvitation" | i18n }} <button
</button> type="button"
<button bitMenuItem
type="button" *ngIf="c.status === emergencyAccessStatusType.Invited"
bitMenuItem (click)="reinvite(c)"
*ngIf="c.status === emergencyAccessStatusType.Accepted" >
(click)="confirm(c)" <i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
> {{ "resendInvitation" | i18n }}
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i> </button>
{{ "confirm" | i18n }} <button
</button> type="button"
<button bitMenuItem
type="button" *ngIf="c.status === emergencyAccessStatusType.Accepted"
bitMenuItem (click)="confirm(c)"
*ngIf="c.status === emergencyAccessStatusType.RecoveryInitiated" >
(click)="approve(c)" <i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
> {{ "confirm" | i18n }}
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i> </button>
{{ "approve" | i18n }} <button
</button> type="button"
<button bitMenuItem
type="button" *ngIf="c.status === emergencyAccessStatusType.RecoveryInitiated"
bitMenuItem (click)="approve(c)"
*ngIf=" >
c.status === emergencyAccessStatusType.RecoveryInitiated || <i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
c.status === emergencyAccessStatusType.RecoveryApproved {{ "approve" | i18n }}
" </button>
(click)="reject(c)" <button
> type="button"
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i> bitMenuItem
{{ "reject" | i18n }} *ngIf="
</button> c.status === emergencyAccessStatusType.RecoveryInitiated ||
<button type="button" bitMenuItem (click)="remove(c)"> c.status === emergencyAccessStatusType.RecoveryApproved
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i> "
{{ "remove" | i18n }} (click)="reject(c)"
</button> >
</bit-menu> <i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
</td> {{ "reject" | i18n }}
</tr> </button>
</tbody> <button type="button" bitMenuItem (click)="remove(c)">
</table> <i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
{{ "remove" | i18n }}
<ng-container *ngIf="!trustedContacts || !trustedContacts.length"> </button>
<p *ngIf="loaded">{{ "noTrustedContacts" | i18n }}</p> </bit-menu>
<ng-container *ngIf="!loaded"> </td>
<i </tr>
class="bwi bwi-spinner bwi-spin text-muted" </ng-template>
title="{{ 'loading' | i18n }}" </bit-table>
aria-hidden="true" <ng-container *ngIf="!trustedContacts || !trustedContacts.length">
></i> <p bitTypography="body1" class="tw-mt-2" *ngIf="loaded">{{ "noTrustedContacts" | i18n }}</p>
<span class="sr-only">{{ "loading" | i18n }}</span> <ng-container *ngIf="!loaded">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</ng-container>
</ng-container> </ng-container>
</ng-container> </bit-section>
<div class="page-header spaced-header"> <bit-section>
<h2>{{ "designatedEmergencyContacts" | i18n }}</h2> <h2 bitTypography="h2">{{ "designatedEmergencyContacts" | i18n }}</h2>
</div>
<table <bit-table *ngIf="grantedContacts && grantedContacts.length">
class="table table-hover table-list mb-0" <ng-container header>
*ngIf="grantedContacts && grantedContacts.length" <tr>
> <th bitCell>{{ "name" | i18n }}</th>
<tbody> <th bitCell>{{ "accessLevel" | i18n }}</th>
<tr *ngFor="let c of grantedContacts; let i = index"> <th bitCell class="tw-text-right">{{ "options" | i18n }}</th>
<td width="30"> </tr>
<bit-avatar </ng-container>
[text]="c | userName" <ng-template body>
[id]="c.grantorId" <tr bitRow *ngFor="let c of grantedContacts; let i = index">
[color]="c.avatarColor" <td bitCell class="tw-flex tw-items-center tw-gap-4">
size="small" <bit-avatar
></bit-avatar> [text]="c | userName"
</td> [id]="c.grantorId"
<td> [color]="c.avatarColor"
<span>{{ c.email }}</span> size="small"
<span bitBadge *ngIf="c.status === emergencyAccessStatusType.Invited">{{ ></bit-avatar>
"invited" | i18n <span>
}}</span> <span>{{ c.email }}</span>
<span <span bitBadge *ngIf="c.status === emergencyAccessStatusType.Invited">{{
bitBadge "invited" | i18n
variant="warning" }}</span>
*ngIf="c.status === emergencyAccessStatusType.Accepted" <span
>{{ "accepted" | i18n }}</span bitBadge
> variant="warning"
<span *ngIf="c.status === emergencyAccessStatusType.Accepted"
bitBadge >{{ "accepted" | i18n }}</span
variant="warning" >
*ngIf="c.status === emergencyAccessStatusType.RecoveryInitiated" <span
>{{ "emergencyAccessRecoveryInitiated" | i18n }}</span bitBadge
> variant="warning"
<span *ngIf="c.status === emergencyAccessStatusType.RecoveryInitiated"
bitBadge >{{ "emergencyAccessRecoveryInitiated" | i18n }}</span
variant="success" >
*ngIf="c.status === emergencyAccessStatusType.RecoveryApproved" <span
>{{ "emergencyAccessRecoveryApproved" | i18n }}</span bitBadge
> variant="success"
*ngIf="c.status === emergencyAccessStatusType.RecoveryApproved"
>{{ "emergencyAccessRecoveryApproved" | i18n }}</span
>
<span bitBadge *ngIf="c.type === emergencyAccessType.View">{{ "view" | i18n }}</span> <small class="tw-text-muted tw-block" *ngIf="c.name">{{ c.name }}</small>
<span bitBadge *ngIf="c.type === emergencyAccessType.Takeover">{{ </span>
"takeover" | i18n </td>
}}</span> <td bitCell>
<span bitBadge *ngIf="c.type === emergencyAccessType.View">{{ "view" | i18n }}</span>
<small class="text-muted d-block" *ngIf="c.name">{{ c.name }}</small> <span bitBadge *ngIf="c.type === emergencyAccessType.Takeover">{{
</td> "takeover" | i18n
<td class="table-list-options"> }}</span>
<button </td>
[bitMenuTriggerFor]="grantedContactOptions" <td bitCell class="tw-text-right">
class="tw-border-none tw-bg-transparent tw-text-main"
type="button"
appA11yTitle="{{ 'options' | i18n }}"
>
<i class="bwi bwi-ellipsis-v bwi-lg" aria-hidden="true"></i>
</button>
<bit-menu #grantedContactOptions>
<button <button
[bitMenuTriggerFor]="grantedContactOptions"
type="button" type="button"
bitMenuItem appA11yTitle="{{ 'options' | i18n }}"
*ngIf="c.status === emergencyAccessStatusType.Confirmed" bitIconButton="bwi-ellipsis-v"
(click)="requestAccess(c)" buttonType="main"
> ></button>
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i> <bit-menu #grantedContactOptions>
{{ "requestAccess" | i18n }} <button
</button> type="button"
<button bitMenuItem
type="button" *ngIf="c.status === emergencyAccessStatusType.Confirmed"
bitMenuItem (click)="requestAccess(c)"
*ngIf=" >
c.status === emergencyAccessStatusType.RecoveryApproved && <i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
c.type === emergencyAccessType.Takeover {{ "requestAccess" | i18n }}
" </button>
(click)="takeover(c)" <button
> type="button"
<i class="bwi bwi-fw bwi-key" aria-hidden="true"></i> bitMenuItem
{{ "takeover" | i18n }} *ngIf="
</button> c.status === emergencyAccessStatusType.RecoveryApproved &&
<button c.type === emergencyAccessType.Takeover
type="button" "
bitMenuItem (click)="takeover(c)"
*ngIf=" >
c.status === emergencyAccessStatusType.RecoveryApproved && <i class="bwi bwi-fw bwi-key" aria-hidden="true"></i>
c.type === emergencyAccessType.View {{ "takeover" | i18n }}
" </button>
[routerLink]="c.id" <button
> type="button"
<i class="bwi bwi-fw bwi-eye" aria-hidden="true"></i> bitMenuItem
{{ "view" | i18n }} *ngIf="
</button> c.status === emergencyAccessStatusType.RecoveryApproved &&
<button type="button" bitMenuItem (click)="remove(c)"> c.type === emergencyAccessType.View
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i> "
{{ "remove" | i18n }} [routerLink]="c.id"
</button> >
</bit-menu> <i class="bwi bwi-fw bwi-eye" aria-hidden="true"></i>
</td> {{ "view" | i18n }}
</tr> </button>
</tbody> <button type="button" bitMenuItem (click)="remove(c)">
</table> <i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
{{ "remove" | i18n }}
<ng-container *ngIf="!grantedContacts || !grantedContacts.length"> </button>
<p *ngIf="loaded">{{ "noGrantedAccess" | i18n }}</p> </bit-menu>
<ng-container *ngIf="!loaded"> </td>
<i </tr>
class="bwi bwi-spinner bwi-spin text-muted" </ng-template>
title="{{ 'loading' | i18n }}" </bit-table>
aria-hidden="true" <ng-container *ngIf="!grantedContacts || !grantedContacts.length">
></i> <p bitTypography="body1" *ngIf="loaded">{{ "noGrantedAccess" | i18n }}</p>
<span class="sr-only">{{ "loading" | i18n }}</span> <ng-container *ngIf="!loaded">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</ng-container>
</ng-container> </ng-container>
</ng-container> </bit-section>
</bit-container> </bit-container>
<ng-template #addEdit></ng-template> <ng-template #addEdit></ng-template>

View File

@@ -1,7 +1,7 @@
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { lastValueFrom } from "rxjs";
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -18,9 +18,18 @@ import {
GrantorEmergencyAccess, GrantorEmergencyAccess,
} from "../../emergency-access/models/emergency-access"; } from "../../emergency-access/models/emergency-access";
import { EmergencyAccessConfirmComponent } from "./confirm/emergency-access-confirm.component"; import {
import { EmergencyAccessAddEditComponent } from "./emergency-access-add-edit.component"; EmergencyAccessConfirmComponent,
import { EmergencyAccessTakeoverComponent } from "./takeover/emergency-access-takeover.component"; EmergencyAccessConfirmDialogResult,
} from "./confirm/emergency-access-confirm.component";
import {
EmergencyAccessAddEditComponent,
EmergencyAccessAddEditDialogResult,
} from "./emergency-access-add-edit.component";
import {
EmergencyAccessTakeoverComponent,
EmergencyAccessTakeoverResultType,
} from "./takeover/emergency-access-takeover.component";
@Component({ @Component({
selector: "emergency-access", selector: "emergency-access",
@@ -46,7 +55,6 @@ export class EmergencyAccessComponent implements OnInit {
constructor( constructor(
private emergencyAccessService: EmergencyAccessService, private emergencyAccessService: EmergencyAccessService,
private i18nService: I18nService, private i18nService: I18nService,
private modalService: ModalService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private messagingService: MessagingService, private messagingService: MessagingService,
private userNamePipe: UserNamePipe, private userNamePipe: UserNamePipe,
@@ -78,37 +86,26 @@ export class EmergencyAccessComponent implements OnInit {
} }
} }
async edit(details: GranteeEmergencyAccess) { edit = async (details: GranteeEmergencyAccess) => {
const [modal] = await this.modalService.openViewRef( const dialogRef = EmergencyAccessAddEditComponent.open(this.dialogService, {
EmergencyAccessAddEditComponent, data: {
this.addEditModalRef, name: this.userNamePipe.transform(details),
(comp) => { emergencyAccessId: details?.id,
comp.name = this.userNamePipe.transform(details); readOnly: !this.canAccessPremium,
comp.emergencyAccessId = details?.id;
comp.readOnly = !this.canAccessPremium;
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
comp.onSaved.subscribe(() => {
modal.close();
// 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.load();
});
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
comp.onDeleted.subscribe(() => {
modal.close();
// 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.remove(details);
});
}, },
); });
}
invite() { const result = await lastValueFrom(dialogRef.closed);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. if (result === EmergencyAccessAddEditDialogResult.Saved) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises await this.load();
this.edit(null); } else if (result === EmergencyAccessAddEditDialogResult.Deleted) {
} await this.remove(details);
}
};
invite = async () => {
await this.edit(null);
};
async reinvite(contact: GranteeEmergencyAccess) { async reinvite(contact: GranteeEmergencyAccess) {
if (this.actionPromise != null) { if (this.actionPromise != null) {
@@ -135,29 +132,23 @@ export class EmergencyAccessComponent implements OnInit {
const autoConfirm = await this.stateService.getAutoConfirmFingerPrints(); const autoConfirm = await this.stateService.getAutoConfirmFingerPrints();
if (autoConfirm == null || !autoConfirm) { if (autoConfirm == null || !autoConfirm) {
const [modal] = await this.modalService.openViewRef( const dialogRef = EmergencyAccessConfirmComponent.open(this.dialogService, {
EmergencyAccessConfirmComponent, data: {
this.confirmModalRef, name: this.userNamePipe.transform(contact),
(comp) => { emergencyAccessId: contact.id,
comp.name = this.userNamePipe.transform(contact); userId: contact?.granteeId,
comp.emergencyAccessId = contact.id;
comp.userId = contact?.granteeId;
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
comp.onConfirmed.subscribe(async () => {
modal.close();
comp.formPromise = this.emergencyAccessService.confirm(contact.id, contact.granteeId);
await comp.formPromise;
updateUser();
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(contact)),
);
});
}, },
); });
const result = await lastValueFrom(dialogRef.closed);
if (result === EmergencyAccessConfirmDialogResult.Confirmed) {
await this.emergencyAccessService.confirm(contact.id, contact.granteeId);
updateUser();
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(contact)),
);
}
return; return;
} }
@@ -267,27 +258,23 @@ export class EmergencyAccessComponent implements OnInit {
); );
} }
async takeover(details: GrantorEmergencyAccess) { takeover = async (details: GrantorEmergencyAccess) => {
const [modal] = await this.modalService.openViewRef( const dialogRef = EmergencyAccessTakeoverComponent.open(this.dialogService, {
EmergencyAccessTakeoverComponent, data: {
this.takeoverModalRef, name: this.userNamePipe.transform(details),
(comp) => { email: details.email,
comp.name = this.userNamePipe.transform(details); emergencyAccessId: details.id ?? null,
comp.email = details.email;
comp.emergencyAccessId = details != null ? details.id : null;
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
comp.onDone.subscribe(() => {
modal.close();
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("passwordResetFor", this.userNamePipe.transform(details)),
);
});
}, },
); });
} const result = await lastValueFrom(dialogRef.closed);
if (result === EmergencyAccessTakeoverResultType.Done) {
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("passwordResetFor", this.userNamePipe.transform(details)),
);
}
};
private removeGrantee(details: GranteeEmergencyAccess) { private removeGrantee(details: GranteeEmergencyAccess) {
const index = this.trustedContacts.indexOf(details); const index = this.trustedContacts.indexOf(details);

View File

@@ -1,79 +1,54 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="userAddEditTitle"> <form [formGroup]="takeoverForm" [bitSubmit]="submit">
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document"> <bit-dialog dialogSize="large">
<form <span bitDialogTitle>
class="modal-content" {{ "takeover" | i18n }}
#form <small class="tw-text-muted" *ngIf="params.name">{{ params.name }}</small>
(ngSubmit)="submit()" </span>
[appApiAction]="formPromise" <div bitDialogContent>
ngNativeValidate <app-callout type="warning">{{ "loggedOutWarning" | i18n }}</app-callout>
> <auth-password-callout [policy]="enforcedPolicyOptions" *ngIf="enforcedPolicyOptions">
<div class="modal-header"> </auth-password-callout>
<h1 class="modal-title" id="userAddEditTitle"> <div class="tw-w-full tw-flex tw-gap-4">
{{ "takeover" | i18n }} <div class="tw-relative tw-flex-1">
<small class="text-muted" *ngIf="name">{{ name }}</small> <bit-form-field disableMargin class="tw-mb-2">
</h1> <bit-label>{{ "newMasterPass" | i18n }}</bit-label>
<button <input
type="button" bitInput
class="close" type="password"
data-dismiss="modal" autocomplete="new-password"
appA11yTitle="{{ 'close' | i18n }}" formControlName="masterPassword"
> />
<span aria-hidden="true">&times;</span> <button type="button" bitSuffix bitIconButton bitPasswordInputToggle></button>
</button> </bit-form-field>
</div> <app-password-strength
<div class="modal-body"> [password]="takeoverForm.value.masterPassword"
<app-callout type="warning">{{ "loggedOutWarning" | i18n }}</app-callout> [email]="email"
<auth-password-callout [policy]="enforcedPolicyOptions" *ngIf="enforcedPolicyOptions"> [showText]="true"
</auth-password-callout> (passwordStrengthResult)="getStrengthResult($event)"
<div class="row"> >
<div class="col-6"> </app-password-strength>
<div class="form-group"> </div>
<label for="masterPassword">{{ "newMasterPass" | i18n }}</label> <div class="tw-relative tw-flex-1">
<input <bit-form-field disableMargin class="tw-mb-2">
id="masterPassword" <bit-label>{{ "confirmNewMasterPass" | i18n }}</bit-label>
type="password" <input
name="NewMasterPasswordHash" bitInput
class="form-control mb-1" type="password"
[(ngModel)]="masterPassword" autocomplete="new-password"
required formControlName="masterPasswordRetype"
appInputVerbatim />
autocomplete="new-password" <button type="button" bitSuffix bitIconButton bitPasswordInputToggle></button>
/> </bit-form-field>
<app-password-strength
[password]="masterPassword"
[email]="email"
[showText]="true"
(passwordStrengthResult)="getStrengthResult($event)"
>
</app-password-strength>
</div>
</div>
<div class="col-6">
<div class="form-group">
<label for="masterPasswordRetype">{{ "confirmNewMasterPass" | i18n }}</label>
<input
id="masterPasswordRetype"
type="password"
name="MasterPasswordRetype"
class="form-control"
[(ngModel)]="masterPasswordRetype"
required
appInputVerbatim
autocomplete="new-password"
/>
</div>
</div>
</div> </div>
</div> </div>
<div class="modal-footer"> </div>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading"> <div bitDialogFooter>
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i> <button type="submit" bitButton bitFormButton buttonType="primary">
<span>{{ "save" | i18n }}</span> {{ "save" | i18n }}
</button> </button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal"> <button bitButton bitFormButton type="button" buttonType="secondary" bitDialogClose>
{{ "cancel" | i18n }} {{ "cancel" | i18n }}
</button> </button>
</div> </div>
</form> </bit-dialog>
</div> </form>
</div>

View File

@@ -1,4 +1,6 @@
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, OnDestroy, OnInit, Inject, Input } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { takeUntil } from "rxjs"; import { takeUntil } from "rxjs";
import { ChangePasswordComponent } from "@bitwarden/angular/auth/components/change-password.component"; import { ChangePasswordComponent } from "@bitwarden/angular/auth/components/change-password.component";
@@ -15,6 +17,17 @@ import { DialogService } from "@bitwarden/components";
import { EmergencyAccessService } from "../../../emergency-access"; import { EmergencyAccessService } from "../../../emergency-access";
export enum EmergencyAccessTakeoverResultType {
Done = "done",
}
type EmergencyAccessTakeoverDialogData = {
/** display name of the account requesting emergency access takeover */
name: string;
/** email of the account requesting emergency access takeover */
email: string;
/** traces a unique emergency request */
emergencyAccessId: string;
};
@Component({ @Component({
selector: "emergency-access-takeover", selector: "emergency-access-takeover",
templateUrl: "emergency-access-takeover.component.html", templateUrl: "emergency-access-takeover.component.html",
@@ -24,16 +37,16 @@ export class EmergencyAccessTakeoverComponent
extends ChangePasswordComponent extends ChangePasswordComponent
implements OnInit, OnDestroy implements OnInit, OnDestroy
{ {
@Output() onDone = new EventEmitter();
@Input() emergencyAccessId: string;
@Input() name: string;
@Input() email: string;
@Input() kdf: KdfType; @Input() kdf: KdfType;
@Input() kdfIterations: number; @Input() kdfIterations: number;
takeoverForm = this.formBuilder.group({
formPromise: Promise<any>; masterPassword: ["", [Validators.required]],
masterPasswordRetype: ["", [Validators.required]],
});
constructor( constructor(
@Inject(DIALOG_DATA) protected params: EmergencyAccessTakeoverDialogData,
private formBuilder: FormBuilder,
i18nService: I18nService, i18nService: I18nService,
cryptoService: CryptoService, cryptoService: CryptoService,
messagingService: MessagingService, messagingService: MessagingService,
@@ -44,6 +57,7 @@ export class EmergencyAccessTakeoverComponent
private emergencyAccessService: EmergencyAccessService, private emergencyAccessService: EmergencyAccessService,
private logService: LogService, private logService: LogService,
dialogService: DialogService, dialogService: DialogService,
private dialogRef: DialogRef<EmergencyAccessTakeoverResultType>,
) { ) {
super( super(
i18nService, i18nService,
@@ -58,7 +72,9 @@ export class EmergencyAccessTakeoverComponent
} }
async ngOnInit() { async ngOnInit() {
const policies = await this.emergencyAccessService.getGrantorPolicies(this.emergencyAccessId); const policies = await this.emergencyAccessService.getGrantorPolicies(
this.params.emergencyAccessId,
);
this.policyService this.policyService
.masterPasswordPolicyOptions$(policies) .masterPasswordPolicyOptions$(policies)
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
@@ -70,18 +86,23 @@ export class EmergencyAccessTakeoverComponent
super.ngOnDestroy(); super.ngOnDestroy();
} }
async submit() { submit = async () => {
if (this.takeoverForm.invalid) {
this.takeoverForm.markAllAsTouched();
return;
}
this.masterPassword = this.takeoverForm.get("masterPassword").value;
this.masterPasswordRetype = this.takeoverForm.get("masterPasswordRetype").value;
if (!(await this.strongPassword())) { if (!(await this.strongPassword())) {
return; return;
} }
try { try {
await this.emergencyAccessService.takeover( await this.emergencyAccessService.takeover(
this.emergencyAccessId, this.params.emergencyAccessId,
this.masterPassword, this.masterPassword,
this.email, this.params.email,
); );
this.onDone.emit();
} catch (e) { } catch (e) {
this.logService.error(e); this.logService.error(e);
this.platformUtilsService.showToast( this.platformUtilsService.showToast(
@@ -90,5 +111,20 @@ export class EmergencyAccessTakeoverComponent
this.i18nService.t("unexpectedError"), this.i18nService.t("unexpectedError"),
); );
} }
} this.dialogRef.close(EmergencyAccessTakeoverResultType.Done);
};
/**
* Strongly typed helper to open a EmergencyAccessTakeoverComponent
* @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<EmergencyAccessTakeoverDialogData>,
) => {
return dialogService.open<EmergencyAccessTakeoverResultType>(
EmergencyAccessTakeoverComponent,
config,
);
};
} }

View File

@@ -1,71 +1,76 @@
<div class="page-header"> <h1 bitTypography="h1">{{ "vault" | i18n }}</h1>
<h1>{{ "vault" | i18n }}</h1>
</div> <div class="tw-mt-6">
<div class="mt-4">
<ng-container *ngIf="ciphers.length"> <ng-container *ngIf="ciphers.length">
<table class="table table-hover table-list table-ciphers"> <bit-table>
<tbody> <ng-template body>
<tr *ngFor="let c of ciphers"> <tr bitRow *ngFor="let currentCipher of ciphers">
<td class="table-list-icon"> <td bitCell>
<app-vault-icon [cipher]="c"></app-vault-icon> <app-vault-icon [cipher]="currentCipher"></app-vault-icon>
</td> </td>
<td class="reduced-lh wrap"> <td bitCell class="tw-w-full">
<a href="#" appStopClick (click)="selectCipher(c)" title="{{ 'editItem' | i18n }}">{{ <a
c.name bitLink
}}</a> href="#"
<ng-container *ngIf="c.organizationId"> appStopClick
(click)="selectCipher(currentCipher)"
title="{{ 'editItem' | i18n }}"
>{{ currentCipher.name }}</a
>
<ng-container *ngIf="currentCipher.organizationId">
<i <i
class="bwi bwi-collection" class="bwi bwi-collection"
appStopProp appStopProp
title="{{ 'shared' | i18n }}" title="{{ 'shared' | i18n }}"
aria-hidden="true" aria-hidden="true"
></i> ></i>
<span class="sr-only">{{ "shared" | i18n }}</span> <span class="tw-sr-only">{{ "shared" | i18n }}</span>
</ng-container> </ng-container>
<ng-container *ngIf="c.hasAttachments"> <ng-container *ngIf="currentCipher.hasAttachments">
<i <i
class="bwi bwi-paperclip" class="bwi bwi-paperclip"
appStopProp appStopProp
title="{{ 'attachments' | i18n }}" title="{{ 'attachments' | i18n }}"
aria-hidden="true" aria-hidden="true"
></i> ></i>
<span class="sr-only">{{ "attachments" | i18n }}</span> <span class="tw-sr-only">{{ "attachments" | i18n }}</span>
</ng-container> </ng-container>
<br /> <br />
<small>{{ c.subTitle }}</small> <small class="tw-text-xs">{{ currentCipher.subTitle }}</small>
</td> </td>
<td class="table-list-options"> <td bitCell>
<div class="dropdown" appListDropdown *ngIf="c.hasAttachments"> <div *ngIf="currentCipher.hasAttachments">
<button <button
class="btn btn-outline-secondary dropdown-toggle" [bitMenuTriggerFor]="optionsMenu"
type="button" type="button"
id="dropdownMenuButton" buttonType="main"
data-toggle="dropdown" bitIconButton="bwi-ellipsis-v"
aria-haspopup="true"
aria-expanded="false"
appA11yTitle="{{ 'options' | i18n }}" appA11yTitle="{{ 'options' | i18n }}"
> ></button>
<i class="bwi bwi-cog bwi-lg" aria-hidden="true"></i> <bit-menu #optionsMenu>
</button> <button
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton"> type="button"
<a class="dropdown-item" href="#" appStopClick (click)="viewAttachments(c)"> bitMenuItem
appStopClick
(click)="viewAttachments(currentCipher)"
>
<i class="bwi bwi-fw bwi-paperclip" aria-hidden="true"></i> <i class="bwi bwi-fw bwi-paperclip" aria-hidden="true"></i>
{{ "attachments" | i18n }} {{ "attachments" | i18n }}
</a> </button>
</div> </bit-menu>
</div> </div>
</td> </td>
</tr> </tr>
</tbody> </ng-template>
</table> </bit-table>
</ng-container> </ng-container>
<ng-container *ngIf="!loaded"> <ng-container *ngIf="!loaded">
<i <i
class="bwi bwi-spinner bwi-spin text-muted" class="bwi bwi-spinner bwi-spin tw-text-muted"
title="{{ 'loading' | i18n }}" title="{{ 'loading' | i18n }}"
aria-hidden="true" aria-hidden="true"
></i> ></i>
<span class="sr-only">{{ "loading" | i18n }}</span> <span class="tw-sr-only">{{ "loading" | i18n }}</span>
</ng-container> </ng-container>
</div> </div>
<ng-template #cipherAddEdit></ng-template> <ng-template #cipherAddEdit></ng-template>

View File

@@ -578,6 +578,9 @@
"access": { "access": {
"message": "Access" "message": "Access"
}, },
"accessLevel": {
"message": "Access level"
},
"loggedOut": { "loggedOut": {
"message": "Logged out" "message": "Logged out"
}, },

View File

@@ -678,7 +678,7 @@ import { ModalService } from "./modal.service";
{ {
provide: PolicyServiceAbstraction, provide: PolicyServiceAbstraction,
useClass: PolicyService, useClass: PolicyService,
deps: [StateServiceAbstraction, StateProvider, OrganizationServiceAbstraction], deps: [StateProvider, OrganizationServiceAbstraction],
}, },
{ {
provide: InternalPolicyService, provide: InternalPolicyService,

View File

@@ -1,7 +1,7 @@
import { DatePipe } from "@angular/common"; import { DatePipe } from "@angular/common";
import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms"; import { FormBuilder, Validators } from "@angular/forms";
import { Subject, takeUntil } from "rxjs"; import { BehaviorSubject, Subject, concatMap, firstValueFrom, map, takeUntil } from "rxjs";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { PolicyType } from "@bitwarden/common/admin-console/enums";
@@ -151,8 +151,11 @@ export class AddEditComponent implements OnInit, OnDestroy {
}); });
this.policyService this.policyService
.policyAppliesToActiveUser$(PolicyType.SendOptions, (p) => p.data.disableHideEmail) .getAll$(PolicyType.SendOptions)
.pipe(takeUntil(this.destroy$)) .pipe(
map((policies) => policies?.some((p) => p.data.disableHideEmail)),
takeUntil(this.destroy$),
)
.subscribe((policyAppliesToActiveUser) => { .subscribe((policyAppliesToActiveUser) => {
if ( if (
(this.disableHideEmail = policyAppliesToActiveUser) && (this.disableHideEmail = policyAppliesToActiveUser) &&
@@ -164,7 +167,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
} }
}); });
this.formGroup.controls.type.valueChanges.subscribe((val) => { this.formGroup.controls.type.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((val) => {
this.type = val; this.type = val;
this.typeChanged(); this.typeChanged();
}); });
@@ -207,28 +210,46 @@ export class AddEditComponent implements OnInit, OnDestroy {
this.type = !this.canAccessPremium || !this.emailVerified ? SendType.Text : SendType.File; this.type = !this.canAccessPremium || !this.emailVerified ? SendType.Text : SendType.File;
if (this.send == null) { if (this.send == null) {
if (this.editMode) { const send = new BehaviorSubject<SendView>(this.send);
const send = this.loadSend(); send.subscribe({
this.send = await send.decrypt(); next: (decryptedSend: SendView | undefined) => {
this.type = this.send.type; if (!(decryptedSend instanceof SendView)) {
this.updateFormValues(); return;
} else { }
this.send = new SendView();
this.send.type = this.type;
this.send.file = new SendFileView();
this.send.text = new SendTextView();
this.send.deletionDate = new Date();
this.send.deletionDate.setDate(this.send.deletionDate.getDate() + 7);
this.formGroup.controls.type.patchValue(this.send.type);
this.formGroup.patchValue({ this.send = decryptedSend;
selectedDeletionDatePreset: DatePreset.SevenDays, decryptedSend.type = decryptedSend.type ?? this.type;
selectedExpirationDatePreset: DatePreset.Never, this.type = this.send.type;
}); this.updateFormValues();
this.hasPassword = this.send.password != null && this.send.password.trim() !== "";
},
error: (error: unknown) => {
const errorMessage = (error as Error).message ?? "An unknown error occurred";
this.logService.error("Failed to decrypt send: " + errorMessage);
},
});
if (this.editMode) {
this.sendService
.get$(this.sendId)
.pipe(
//Promise.reject will complete the BehaviourSubject, if desktop starts relying only on BehaviourSubject, this should be changed.
concatMap((s) =>
s instanceof Send ? s.decrypt() : Promise.reject(new Error("Failed to load send.")),
),
takeUntil(this.destroy$),
)
.subscribe(send);
} else {
const sendView = new SendView();
sendView.type = this.type;
sendView.file = new SendFileView();
sendView.text = new SendTextView();
sendView.deletionDate = new Date();
sendView.deletionDate.setDate(sendView.deletionDate.getDate() + 7);
send.next(sendView);
} }
} }
this.hasPassword = this.send.password != null && this.send.password.trim() !== "";
} }
async submit(): Promise<boolean> { async submit(): Promise<boolean> {
@@ -373,8 +394,8 @@ export class AddEditComponent implements OnInit, OnDestroy {
this.showOptions = !this.showOptions; this.showOptions = !this.showOptions;
} }
protected loadSend(): Send { protected loadSend(): Promise<Send> {
return this.sendService.get(this.sendId); return firstValueFrom(this.sendService.get$(this.sendId));
} }
protected async encryptSend(file: File): Promise<[Send, EncArrayBuffer]> { protected async encryptSend(file: File): Promise<[Send, EncArrayBuffer]> {

View File

@@ -1,6 +1,7 @@
import { Observable } from "rxjs"; import { Observable } from "rxjs";
import { ListResponse } from "../../../models/response/list.response"; import { ListResponse } from "../../../models/response/list.response";
import { UserId } from "../../../types/guid";
import { PolicyType } from "../../enums"; import { PolicyType } from "../../enums";
import { PolicyData } from "../../models/data/policy.data"; import { PolicyData } from "../../models/data/policy.data";
import { MasterPasswordPolicyOptions } from "../../models/domain/master-password-policy-options"; import { MasterPasswordPolicyOptions } from "../../models/domain/master-password-policy-options";
@@ -10,9 +11,9 @@ import { PolicyResponse } from "../../models/response/policy.response";
export abstract class PolicyService { export abstract class PolicyService {
/** /**
* All {@link Policy} objects for the active user (from sync data). * All policies for the active user from sync data.
* May include policies that are disabled or otherwise do not apply to the user. * May include policies that are disabled or otherwise do not apply to the user. Be careful using this!
* @see {@link get$} or {@link policyAppliesToActiveUser$} if you want to know when a policy applies to a user. * Consider using {@link get$} or {@link getAll$} instead, which will only return policies that should be enforced against the user.
*/ */
policies$: Observable<Policy[]>; policies$: Observable<Policy[]>;
@@ -20,37 +21,33 @@ export abstract class PolicyService {
* @returns the first {@link Policy} found that applies to the active user. * @returns the first {@link Policy} found that applies to the active user.
* A policy "applies" if it is enabled and the user is not exempt (e.g. because they are an Owner). * A policy "applies" if it is enabled and the user is not exempt (e.g. because they are an Owner).
* @param policyType the {@link PolicyType} to search for * @param policyType the {@link PolicyType} to search for
* @param policyFilter Optional predicate to apply when filtering policies * @see {@link getAll$} if you need all policies of a given type
*/ */
get$: (policyType: PolicyType, policyFilter?: (policy: Policy) => boolean) => Observable<Policy>; get$: (policyType: PolicyType) => Observable<Policy>;
/**
* @returns all {@link Policy} objects of a given type that apply to the specified user (or the active user if not specified).
* A policy "applies" if it is enabled and the user is not exempt (e.g. because they are an Owner).
* @param policyType the {@link PolicyType} to search for
*/
getAll$: (policyType: PolicyType, userId?: UserId) => Observable<Policy[]>;
/** /**
* All {@link Policy} objects for the specified user (from sync data). * All {@link Policy} objects for the specified user (from sync data).
* May include policies that are disabled or otherwise do not apply to the user. * May include policies that are disabled or otherwise do not apply to the user.
* @see {@link policyAppliesToUser} if you want to know when a policy applies to the user. * Consider using {@link getAll$} instead, which will only return policies that should be enforced against the user.
* @deprecated Use {@link policies$} instead
*/ */
getAll: (type?: PolicyType, userId?: string) => Promise<Policy[]>; getAll: (policyType: PolicyType) => Promise<Policy[]>;
/** /**
* @returns true if the {@link PolicyType} applies to the current user, otherwise false. * @returns true if a policy of the specified type applies to the active user, otherwise false.
* @remarks A policy "applies" if it is enabled and the user is not exempt (e.g. because they are an Owner). * A policy "applies" if it is enabled and the user is not exempt (e.g. because they are an Owner).
* This does not take into account the policy's configuration - if that is important, use {@link getAll$} to get the
* {@link Policy} objects and then filter by Policy.data.
*/ */
policyAppliesToActiveUser$: ( policyAppliesToActiveUser$: (policyType: PolicyType) => Observable<boolean>;
policyType: PolicyType,
policyFilter?: (policy: Policy) => boolean,
) => Observable<boolean>;
/** policyAppliesToUser: (policyType: PolicyType) => Promise<boolean>;
* @returns true if the {@link PolicyType} applies to the specified user, otherwise false.
* @remarks A policy "applies" if it is enabled and the user is not exempt (e.g. because they are an Owner).
* @see {@link policyAppliesToActiveUser$} if you only want to know about the current user.
*/
policyAppliesToUser: (
policyType: PolicyType,
policyFilter?: (policy: Policy) => boolean,
userId?: string,
) => Promise<boolean>;
// Policy specific interfaces // Policy specific interfaces
@@ -93,7 +90,7 @@ export abstract class PolicyService {
} }
export abstract class InternalPolicyService extends PolicyService { export abstract class InternalPolicyService extends PolicyService {
upsert: (policy: PolicyData) => Promise<any>; upsert: (policy: PolicyData) => Promise<void>;
replace: (policies: { [id: string]: PolicyData }) => Promise<void>; replace: (policies: { [id: string]: PolicyData }) => Promise<void>;
clear: (userId?: string) => Promise<any>; clear: (userId?: string) => Promise<void>;
} }

View File

@@ -1,8 +1,9 @@
import { PolicyId } from "../../../types/guid";
import { PolicyType } from "../../enums"; import { PolicyType } from "../../enums";
import { PolicyResponse } from "../response/policy.response"; import { PolicyResponse } from "../response/policy.response";
export class PolicyData { export class PolicyData {
id: string; id: PolicyId;
organizationId: string; organizationId: string;
type: PolicyType; type: PolicyType;
data: Record<string, string | number | boolean>; data: Record<string, string | number | boolean>;

View File

@@ -1,9 +1,10 @@
import Domain from "../../../platform/models/domain/domain-base"; import Domain from "../../../platform/models/domain/domain-base";
import { PolicyId } from "../../../types/guid";
import { PolicyType } from "../../enums"; import { PolicyType } from "../../enums";
import { PolicyData } from "../data/policy.data"; import { PolicyData } from "../data/policy.data";
export class Policy extends Domain { export class Policy extends Domain {
id: string; id: PolicyId;
organizationId: string; organizationId: string;
type: PolicyType; type: PolicyType;
data: any; data: any;

View File

@@ -1,8 +1,9 @@
import { BaseResponse } from "../../../models/response/base.response"; import { BaseResponse } from "../../../models/response/base.response";
import { PolicyId } from "../../../types/guid";
import { PolicyType } from "../../enums"; import { PolicyType } from "../../enums";
export class PolicyResponse extends BaseResponse { export class PolicyResponse extends BaseResponse {
id: string; id: PolicyId;
organizationId: string; organizationId: string;
type: PolicyType; type: PolicyType;
data: any; data: any;

View File

@@ -1,5 +1,5 @@
import { mock, MockProxy } from "jest-mock-extended"; import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject, firstValueFrom } from "rxjs"; import { firstValueFrom, of } from "rxjs";
import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec"; import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec";
import { FakeActiveUserState } from "../../../../spec/fake-state"; import { FakeActiveUserState } from "../../../../spec/fake-state";
@@ -19,71 +19,51 @@ import { ResetPasswordPolicyOptions } from "../../../admin-console/models/domain
import { PolicyResponse } from "../../../admin-console/models/response/policy.response"; import { PolicyResponse } from "../../../admin-console/models/response/policy.response";
import { POLICIES, PolicyService } from "../../../admin-console/services/policy/policy.service"; import { POLICIES, PolicyService } from "../../../admin-console/services/policy/policy.service";
import { ListResponse } from "../../../models/response/list.response"; import { ListResponse } from "../../../models/response/list.response";
import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
import { ContainerService } from "../../../platform/services/container.service";
import { StateService } from "../../../platform/services/state.service";
import { PolicyId, UserId } from "../../../types/guid"; import { PolicyId, UserId } from "../../../types/guid";
describe("PolicyService", () => { describe("PolicyService", () => {
let policyService: PolicyService;
let cryptoService: MockProxy<CryptoService>;
let stateService: MockProxy<StateService>;
let stateProvider: FakeStateProvider; let stateProvider: FakeStateProvider;
let organizationService: MockProxy<OrganizationService>; let organizationService: MockProxy<OrganizationService>;
let encryptService: MockProxy<EncryptService>; let activeUserState: FakeActiveUserState<Record<PolicyId, PolicyData>>;
let activeAccount: BehaviorSubject<string>;
let activeAccountUnlocked: BehaviorSubject<boolean>; let policyService: PolicyService;
beforeEach(() => { beforeEach(() => {
stateService = mock<StateService>();
const accountService = mockAccountServiceWith("userId" as UserId); const accountService = mockAccountServiceWith("userId" as UserId);
stateProvider = new FakeStateProvider(accountService); stateProvider = new FakeStateProvider(accountService);
organizationService = mock<OrganizationService>(); organizationService = mock<OrganizationService>();
organizationService.getAll
.calledWith("user")
.mockResolvedValue([
new Organization(
organizationData(
"test-organization",
true,
true,
OrganizationUserStatusType.Accepted,
false,
),
),
]);
organizationService.getAll.calledWith(undefined).mockResolvedValue([]);
organizationService.getAll.calledWith(null).mockResolvedValue([]);
activeAccount = new BehaviorSubject("123");
activeAccountUnlocked = new BehaviorSubject(true);
stateService.getDecryptedPolicies.calledWith({ userId: "user" }).mockResolvedValue(null);
stateService.getEncryptedPolicies.calledWith({ userId: "user" }).mockResolvedValue({
"1": policyData("1", "test-organization", PolicyType.MaximumVaultTimeout, true, {
minutes: 14,
}),
});
stateService.getEncryptedPolicies.mockResolvedValue({
"1": policyData("1", "test-organization", PolicyType.MaximumVaultTimeout, true, {
minutes: 14,
}),
});
stateService.activeAccount$ = activeAccount;
stateService.activeAccountUnlocked$ = activeAccountUnlocked;
stateService.getUserId.mockResolvedValue("user");
(window as any).bitwardenContainerService = new ContainerService(cryptoService, encryptService);
policyService = new PolicyService(stateService, stateProvider, organizationService); activeUserState = stateProvider.activeUser.getFake(POLICIES);
}); organizationService.organizations$ = of([
// User
organization("org1", true, true, OrganizationUserStatusType.Confirmed, false),
// Owner
organization(
"org2",
true,
true,
OrganizationUserStatusType.Confirmed,
false,
OrganizationUserType.Owner,
),
// Does not use policies
organization("org3", true, false, OrganizationUserStatusType.Confirmed, false),
// Another User
organization("org4", true, true, OrganizationUserStatusType.Confirmed, false),
// Another User
organization("org5", true, true, OrganizationUserStatusType.Confirmed, false),
]);
afterEach(() => { policyService = new PolicyService(stateProvider, organizationService);
activeAccount.complete();
activeAccountUnlocked.complete();
}); });
it("upsert", async () => { it("upsert", async () => {
activeUserState.nextState(
arrayToRecord([
policyData("1", "test-organization", PolicyType.MaximumVaultTimeout, true, { minutes: 14 }),
]),
);
await policyService.upsert(policyData("99", "test-organization", PolicyType.DisableSend, true)); await policyService.upsert(policyData("99", "test-organization", PolicyType.DisableSend, true));
expect(await firstValueFrom(policyService.policies$)).toEqual([ expect(await firstValueFrom(policyService.policies$)).toEqual([
@@ -104,6 +84,12 @@ describe("PolicyService", () => {
}); });
it("replace", async () => { it("replace", async () => {
activeUserState.nextState(
arrayToRecord([
policyData("1", "test-organization", PolicyType.MaximumVaultTimeout, true, { minutes: 14 }),
]),
);
await policyService.replace({ await policyService.replace({
"2": policyData("2", "test-organization", PolicyType.DisableSend, true), "2": policyData("2", "test-organization", PolicyType.DisableSend, true),
}); });
@@ -118,37 +104,63 @@ describe("PolicyService", () => {
]); ]);
}); });
it("locking should clear", async () => {
activeAccountUnlocked.next(false);
// Sleep for 100ms to avoid timing issues
await new Promise((r) => setTimeout(r, 100));
expect((await firstValueFrom(policyService.policies$)).length).toBe(0);
});
describe("clear", () => { describe("clear", () => {
it("null userId", async () => { beforeEach(() => {
activeUserState.nextState(
arrayToRecord([
policyData("1", "test-organization", PolicyType.MaximumVaultTimeout, true, {
minutes: 14,
}),
]),
);
});
it("clears state for the active user", async () => {
await policyService.clear(); await policyService.clear();
expect(stateService.setEncryptedPolicies).toBeCalledTimes(1); expect(await firstValueFrom(policyService.policies$)).toEqual([]);
expect(await firstValueFrom(activeUserState.state$)).toEqual(null);
expect((await firstValueFrom(policyService.policies$)).length).toBe(0); expect(stateProvider.activeUser.getFake(POLICIES).nextMock).toHaveBeenCalledWith([
"userId",
null,
]);
}); });
it("matching userId", async () => { it("clears state for an inactive user", async () => {
await policyService.clear("user"); const inactiveUserId = "someOtherUserId" as UserId;
const inactiveUserState = stateProvider.singleUser.getFake(inactiveUserId, POLICIES);
inactiveUserState.nextState(
arrayToRecord([
policyData("10", "another-test-organization", PolicyType.PersonalOwnership, true),
]),
);
expect(stateService.setEncryptedPolicies).toBeCalledTimes(1); await policyService.clear(inactiveUserId);
expect((await firstValueFrom(policyService.policies$)).length).toBe(0); // Active user is not affected
}); const expectedActiveUserPolicy: Partial<Policy> = {
id: "1" as PolicyId,
organizationId: "test-organization",
type: PolicyType.MaximumVaultTimeout,
enabled: true,
data: { minutes: 14 },
};
expect(await firstValueFrom(policyService.policies$)).toEqual([expectedActiveUserPolicy]);
expect(await firstValueFrom(activeUserState.state$)).toEqual({
"1": expectedActiveUserPolicy,
});
expect(stateProvider.activeUser.getFake(POLICIES).nextMock).not.toHaveBeenCalled();
it("mismatching userId", async () => { // Non-active user is cleared
await policyService.clear("12"); expect(
await firstValueFrom(
expect(stateService.setEncryptedPolicies).toBeCalledTimes(1); policyService.getAll$(PolicyType.PersonalOwnership, "someOtherUserId" as UserId),
),
expect((await firstValueFrom(policyService.policies$)).length).toBe(1); ).toEqual([]);
expect(await firstValueFrom(inactiveUserState.state$)).toEqual(null);
expect(
stateProvider.singleUser.getFake("someOtherUserId" as UserId, POLICIES).nextMock,
).toHaveBeenCalledWith(null);
}); });
}); });
@@ -313,300 +325,260 @@ describe("PolicyService", () => {
}); });
}); });
describe("get$", () => {
it("returns the specified PolicyType", async () => {
activeUserState.nextState(
arrayToRecord([
policyData("policy1", "org1", PolicyType.ActivateAutofill, true),
policyData("policy2", "org1", PolicyType.DisablePersonalVaultExport, true),
]),
);
const result = await firstValueFrom(
policyService.get$(PolicyType.DisablePersonalVaultExport),
);
expect(result).toEqual({
id: "policy2",
organizationId: "org1",
type: PolicyType.DisablePersonalVaultExport,
enabled: true,
});
});
it("does not return disabled policies", async () => {
activeUserState.nextState(
arrayToRecord([
policyData("policy1", "org1", PolicyType.ActivateAutofill, true),
policyData("policy2", "org1", PolicyType.DisablePersonalVaultExport, false),
]),
);
const result = await firstValueFrom(
policyService.get$(PolicyType.DisablePersonalVaultExport),
);
expect(result).toBeNull();
});
it("does not return policies that do not apply to the user because the user's role is exempt", async () => {
activeUserState.nextState(
arrayToRecord([
policyData("policy1", "org1", PolicyType.ActivateAutofill, true),
policyData("policy2", "org2", PolicyType.DisablePersonalVaultExport, false),
]),
);
const result = await firstValueFrom(
policyService.get$(PolicyType.DisablePersonalVaultExport),
);
expect(result).toBeNull();
});
it("does not return policies for organizations that do not use policies", async () => {
activeUserState.nextState(
arrayToRecord([
policyData("policy1", "org3", PolicyType.ActivateAutofill, true),
policyData("policy2", "org2", PolicyType.DisablePersonalVaultExport, true),
]),
);
const result = await firstValueFrom(policyService.get$(PolicyType.ActivateAutofill));
expect(result).toBeNull();
});
});
describe("getAll$", () => {
it("returns the specified PolicyTypes", async () => {
activeUserState.nextState(
arrayToRecord([
policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true),
policyData("policy2", "org1", PolicyType.ActivateAutofill, true),
policyData("policy3", "org5", PolicyType.DisablePersonalVaultExport, true),
policyData("policy4", "org1", PolicyType.DisablePersonalVaultExport, true),
]),
);
const result = await firstValueFrom(
policyService.getAll$(PolicyType.DisablePersonalVaultExport),
);
expect(result).toEqual([
{
id: "policy1",
organizationId: "org4",
type: PolicyType.DisablePersonalVaultExport,
enabled: true,
},
{
id: "policy3",
organizationId: "org5",
type: PolicyType.DisablePersonalVaultExport,
enabled: true,
},
{
id: "policy4",
organizationId: "org1",
type: PolicyType.DisablePersonalVaultExport,
enabled: true,
},
]);
});
it("does not return disabled policies", async () => {
activeUserState.nextState(
arrayToRecord([
policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true),
policyData("policy2", "org1", PolicyType.ActivateAutofill, true),
policyData("policy3", "org5", PolicyType.DisablePersonalVaultExport, false), // disabled
policyData("policy4", "org1", PolicyType.DisablePersonalVaultExport, true),
]),
);
const result = await firstValueFrom(
policyService.getAll$(PolicyType.DisablePersonalVaultExport),
);
expect(result).toEqual([
{
id: "policy1",
organizationId: "org4",
type: PolicyType.DisablePersonalVaultExport,
enabled: true,
},
{
id: "policy4",
organizationId: "org1",
type: PolicyType.DisablePersonalVaultExport,
enabled: true,
},
]);
});
it("does not return policies that do not apply to the user because the user's role is exempt", async () => {
activeUserState.nextState(
arrayToRecord([
policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true),
policyData("policy2", "org1", PolicyType.ActivateAutofill, true),
policyData("policy3", "org5", PolicyType.DisablePersonalVaultExport, true),
policyData("policy4", "org2", PolicyType.DisablePersonalVaultExport, true), // owner
]),
);
const result = await firstValueFrom(
policyService.getAll$(PolicyType.DisablePersonalVaultExport),
);
expect(result).toEqual([
{
id: "policy1",
organizationId: "org4",
type: PolicyType.DisablePersonalVaultExport,
enabled: true,
},
{
id: "policy3",
organizationId: "org5",
type: PolicyType.DisablePersonalVaultExport,
enabled: true,
},
]);
});
it("does not return policies for organizations that do not use policies", async () => {
activeUserState.nextState(
arrayToRecord([
policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true),
policyData("policy2", "org1", PolicyType.ActivateAutofill, true),
policyData("policy3", "org3", PolicyType.DisablePersonalVaultExport, true), // does not use policies
policyData("policy4", "org1", PolicyType.DisablePersonalVaultExport, true),
]),
);
const result = await firstValueFrom(
policyService.getAll$(PolicyType.DisablePersonalVaultExport),
);
expect(result).toEqual([
{
id: "policy1",
organizationId: "org4",
type: PolicyType.DisablePersonalVaultExport,
enabled: true,
},
{
id: "policy4",
organizationId: "org1",
type: PolicyType.DisablePersonalVaultExport,
enabled: true,
},
]);
});
});
describe("policyAppliesToActiveUser$", () => { describe("policyAppliesToActiveUser$", () => {
it("MasterPassword does not apply", async () => { it("returns true when the policyType applies to the user", async () => {
const result = await firstValueFrom( activeUserState.nextState(
policyService.policyAppliesToActiveUser$(PolicyType.MasterPassword), arrayToRecord([
policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true),
policyData("policy2", "org1", PolicyType.ActivateAutofill, true),
policyData("policy3", "org5", PolicyType.DisablePersonalVaultExport, true),
policyData("policy4", "org1", PolicyType.DisablePersonalVaultExport, true),
]),
); );
expect(result).toEqual(false);
});
it("MaximumVaultTimeout applies", async () => {
const result = await firstValueFrom(
policyService.policyAppliesToActiveUser$(PolicyType.MaximumVaultTimeout),
);
expect(result).toEqual(true);
});
it("PolicyFilter filters result", async () => {
const result = await firstValueFrom(
policyService.policyAppliesToActiveUser$(PolicyType.MaximumVaultTimeout, (p) => false),
);
expect(result).toEqual(false);
});
it("DisablePersonalVaultExport does not apply", async () => {
const result = await firstValueFrom( const result = await firstValueFrom(
policyService.policyAppliesToActiveUser$(PolicyType.DisablePersonalVaultExport), policyService.policyAppliesToActiveUser$(PolicyType.DisablePersonalVaultExport),
); );
expect(result).toEqual(false); expect(result).toBe(true);
}); });
});
describe("policyAppliesToUser", () => { it("returns false when policyType is disabled", async () => {
it("MasterPassword does not apply", async () => { activeUserState.nextState(
const result = await policyService.policyAppliesToUser( arrayToRecord([
PolicyType.MasterPassword, policyData("policy2", "org1", PolicyType.ActivateAutofill, true),
null, policyData("policy3", "org5", PolicyType.DisablePersonalVaultExport, false), // disabled
"user", ]),
); );
expect(result).toEqual(false); const result = await firstValueFrom(
}); policyService.policyAppliesToActiveUser$(PolicyType.DisablePersonalVaultExport),
it("MaximumVaultTimeout applies", async () => {
const result = await policyService.policyAppliesToUser(
PolicyType.MaximumVaultTimeout,
null,
"user",
); );
expect(result).toEqual(true); expect(result).toBe(false);
}); });
it("PolicyFilter filters result", async () => { it("returns false when the policyType does not apply to the user because the user's role is exempt", async () => {
const result = await policyService.policyAppliesToUser( activeUserState.nextState(
PolicyType.MaximumVaultTimeout, arrayToRecord([
(p) => false, policyData("policy2", "org1", PolicyType.ActivateAutofill, true),
"user", policyData("policy4", "org2", PolicyType.DisablePersonalVaultExport, true), // owner
]),
); );
expect(result).toEqual(false); const result = await firstValueFrom(
}); policyService.policyAppliesToActiveUser$(PolicyType.DisablePersonalVaultExport),
it("DisablePersonalVaultExport does not apply", async () => {
const result = await policyService.policyAppliesToUser(
PolicyType.DisablePersonalVaultExport,
null,
"user",
); );
expect(result).toEqual(false); expect(result).toBe(false);
});
});
// TODO: remove this nesting once fully migrated to StateProvider
describe("stateProvider methods", () => {
let policyState$: FakeActiveUserState<Record<PolicyId, PolicyData>>;
beforeEach(() => {
policyState$ = stateProvider.activeUser.getFake(POLICIES);
organizationService.organizations$ = new BehaviorSubject([
// User
organization("org1", true, true, OrganizationUserStatusType.Confirmed, false),
// Owner
organization(
"org2",
true,
true,
OrganizationUserStatusType.Confirmed,
false,
OrganizationUserType.Owner,
),
// Does not use policies
organization("org3", true, false, OrganizationUserStatusType.Confirmed, false),
// Another User
organization("org4", true, true, OrganizationUserStatusType.Confirmed, false),
// Another User
organization("org5", true, true, OrganizationUserStatusType.Confirmed, false),
]);
}); });
describe("get_vNext$", () => { it("returns false for organizations that do not use policies", async () => {
it("returns the specified PolicyType", async () => { activeUserState.nextState(
policyState$.nextState( arrayToRecord([
arrayToRecord([ policyData("policy2", "org1", PolicyType.ActivateAutofill, true),
policyData("policy1", "org1", PolicyType.ActivateAutofill, true), policyData("policy3", "org3", PolicyType.DisablePersonalVaultExport, true), // does not use policies
policyData("policy2", "org1", PolicyType.DisablePersonalVaultExport, true), ]),
]), );
);
const result = await firstValueFrom( const result = await firstValueFrom(
policyService.get_vNext$(PolicyType.DisablePersonalVaultExport), policyService.policyAppliesToActiveUser$(PolicyType.DisablePersonalVaultExport),
); );
expect(result).toEqual({ expect(result).toBe(false);
id: "policy2",
organizationId: "org1",
type: PolicyType.DisablePersonalVaultExport,
enabled: true,
});
});
it("does not return disabled policies", async () => {
policyState$.nextState(
arrayToRecord([
policyData("policy1", "org1", PolicyType.ActivateAutofill, true),
policyData("policy2", "org1", PolicyType.DisablePersonalVaultExport, false),
]),
);
const result = await firstValueFrom(
policyService.get_vNext$(PolicyType.DisablePersonalVaultExport),
);
expect(result).toBeNull();
});
it("does not return policies that do not apply to the user because the user's role is exempt", async () => {
policyState$.nextState(
arrayToRecord([
policyData("policy1", "org1", PolicyType.ActivateAutofill, true),
policyData("policy2", "org2", PolicyType.DisablePersonalVaultExport, false),
]),
);
const result = await firstValueFrom(
policyService.get_vNext$(PolicyType.DisablePersonalVaultExport),
);
expect(result).toBeNull();
});
it("does not return policies for organizations that do not use policies", async () => {
policyState$.nextState(
arrayToRecord([
policyData("policy1", "org3", PolicyType.ActivateAutofill, true),
policyData("policy2", "org2", PolicyType.DisablePersonalVaultExport, true),
]),
);
const result = await firstValueFrom(policyService.get_vNext$(PolicyType.ActivateAutofill));
expect(result).toBeNull();
});
});
describe("getAll_vNext$", () => {
it("returns the specified PolicyTypes", async () => {
policyState$.nextState(
arrayToRecord([
policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true),
policyData("policy2", "org1", PolicyType.ActivateAutofill, true),
policyData("policy3", "org5", PolicyType.DisablePersonalVaultExport, true),
policyData("policy4", "org1", PolicyType.DisablePersonalVaultExport, true),
]),
);
const result = await firstValueFrom(
policyService.getAll_vNext$(PolicyType.DisablePersonalVaultExport),
);
expect(result).toEqual([
{
id: "policy1",
organizationId: "org4",
type: PolicyType.DisablePersonalVaultExport,
enabled: true,
},
{
id: "policy3",
organizationId: "org5",
type: PolicyType.DisablePersonalVaultExport,
enabled: true,
},
{
id: "policy4",
organizationId: "org1",
type: PolicyType.DisablePersonalVaultExport,
enabled: true,
},
]);
});
it("does not return disabled policies", async () => {
policyState$.nextState(
arrayToRecord([
policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true),
policyData("policy2", "org1", PolicyType.ActivateAutofill, true),
policyData("policy3", "org5", PolicyType.DisablePersonalVaultExport, false), // disabled
policyData("policy4", "org1", PolicyType.DisablePersonalVaultExport, true),
]),
);
const result = await firstValueFrom(
policyService.getAll_vNext$(PolicyType.DisablePersonalVaultExport),
);
expect(result).toEqual([
{
id: "policy1",
organizationId: "org4",
type: PolicyType.DisablePersonalVaultExport,
enabled: true,
},
{
id: "policy4",
organizationId: "org1",
type: PolicyType.DisablePersonalVaultExport,
enabled: true,
},
]);
});
it("does not return policies that do not apply to the user because the user's role is exempt", async () => {
policyState$.nextState(
arrayToRecord([
policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true),
policyData("policy2", "org1", PolicyType.ActivateAutofill, true),
policyData("policy3", "org5", PolicyType.DisablePersonalVaultExport, true),
policyData("policy4", "org2", PolicyType.DisablePersonalVaultExport, true), // owner
]),
);
const result = await firstValueFrom(
policyService.getAll_vNext$(PolicyType.DisablePersonalVaultExport),
);
expect(result).toEqual([
{
id: "policy1",
organizationId: "org4",
type: PolicyType.DisablePersonalVaultExport,
enabled: true,
},
{
id: "policy3",
organizationId: "org5",
type: PolicyType.DisablePersonalVaultExport,
enabled: true,
},
]);
});
it("does not return policies for organizations that do not use policies", async () => {
policyState$.nextState(
arrayToRecord([
policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true),
policyData("policy2", "org1", PolicyType.ActivateAutofill, true),
policyData("policy3", "org3", PolicyType.DisablePersonalVaultExport, true), // does not use policies
policyData("policy4", "org1", PolicyType.DisablePersonalVaultExport, true),
]),
);
const result = await firstValueFrom(
policyService.getAll_vNext$(PolicyType.DisablePersonalVaultExport),
);
expect(result).toEqual([
{
id: "policy1",
organizationId: "org4",
type: PolicyType.DisablePersonalVaultExport,
enabled: true,
},
{
id: "policy4",
organizationId: "org1",
type: PolicyType.DisablePersonalVaultExport,
enabled: true,
},
]);
});
}); });
}); });
@@ -618,7 +590,7 @@ describe("PolicyService", () => {
data?: any, data?: any,
) { ) {
const policyData = new PolicyData({} as any); const policyData = new PolicyData({} as any);
policyData.id = id; policyData.id = id as PolicyId;
policyData.organizationId = organizationId; policyData.organizationId = organizationId;
policyData.type = type; policyData.type = type;
policyData.enabled = enabled; policyData.enabled = enabled;

View File

@@ -1,8 +1,6 @@
import { BehaviorSubject, combineLatest, concatMap, map, Observable, of } from "rxjs"; import { combineLatest, firstValueFrom, map, Observable, of } from "rxjs";
import { ListResponse } from "../../../models/response/list.response"; import { ListResponse } from "../../../models/response/list.response";
import { StateService } from "../../../platform/abstractions/state.service";
import { Utils } from "../../../platform/misc/utils";
import { KeyDefinition, POLICIES_DISK, StateProvider } from "../../../platform/state"; import { KeyDefinition, POLICIES_DISK, StateProvider } from "../../../platform/state";
import { PolicyId, UserId } from "../../../types/guid"; import { PolicyId, UserId } from "../../../types/guid";
import { OrganizationService } from "../../abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "../../abstractions/organization/organization.service.abstraction";
@@ -23,42 +21,19 @@ export const POLICIES = KeyDefinition.record<PolicyData, PolicyId>(POLICIES_DISK
}); });
export class PolicyService implements InternalPolicyServiceAbstraction { export class PolicyService implements InternalPolicyServiceAbstraction {
protected _policies: BehaviorSubject<Policy[]> = new BehaviorSubject([]);
policies$ = this._policies.asObservable();
private activeUserPolicyState = this.stateProvider.getActive(POLICIES); private activeUserPolicyState = this.stateProvider.getActive(POLICIES);
activeUserPolicies$ = this.activeUserPolicyState.state$.pipe( private activeUserPolicies$ = this.activeUserPolicyState.state$.pipe(
map((policyData) => policyRecordToArray(policyData)), map((policyData) => policyRecordToArray(policyData)),
); );
policies$ = this.activeUserPolicies$;
constructor( constructor(
protected stateService: StateService,
private stateProvider: StateProvider, private stateProvider: StateProvider,
private organizationService: OrganizationService, private organizationService: OrganizationService,
) { ) {}
this.stateService.activeAccountUnlocked$
.pipe(
concatMap(async (unlocked) => {
if (Utils.global.bitwardenContainerService == null) {
return;
}
if (!unlocked) { get$(policyType: PolicyType) {
this._policies.next([]);
return;
}
const data = await this.stateService.getEncryptedPolicies();
await this.updateObservables(data);
}),
)
.subscribe();
}
// --- StateProvider methods - not yet wired up
get_vNext$(policyType: PolicyType) {
const filteredPolicies$ = this.activeUserPolicies$.pipe( const filteredPolicies$ = this.activeUserPolicies$.pipe(
map((policies) => policies.filter((p) => p.type === policyType)), map((policies) => policies.filter((p) => p.type === policyType)),
); );
@@ -71,7 +46,7 @@ export class PolicyService implements InternalPolicyServiceAbstraction {
); );
} }
getAll_vNext$(policyType: PolicyType, userId?: UserId) { getAll$(policyType: PolicyType, userId?: UserId) {
const filteredPolicies$ = this.stateProvider.getUserState$(POLICIES, userId).pipe( const filteredPolicies$ = this.stateProvider.getUserState$(POLICIES, userId).pipe(
map((policyData) => policyRecordToArray(policyData)), map((policyData) => policyRecordToArray(policyData)),
map((policies) => policies.filter((p) => p.type === policyType)), map((policies) => policies.filter((p) => p.type === policyType)),
@@ -82,8 +57,18 @@ export class PolicyService implements InternalPolicyServiceAbstraction {
); );
} }
policyAppliesToActiveUser_vNext$(policyType: PolicyType) { async getAll(policyType: PolicyType) {
return this.get_vNext$(policyType).pipe(map((policy) => policy != null)); return await firstValueFrom(
this.policies$.pipe(map((policies) => policies.filter((p) => p.type === policyType))),
);
}
policyAppliesToActiveUser$(policyType: PolicyType) {
return this.get$(policyType).pipe(map((policy) => policy != null));
}
async policyAppliesToUser(policyType: PolicyType) {
return await firstValueFrom(this.policyAppliesToActiveUser$(policyType));
} }
private enforcedPolicyFilter(policies: Policy[], organizations: Organization[]) { private enforcedPolicyFilter(policies: Policy[], organizations: Organization[]) {
@@ -105,45 +90,6 @@ export class PolicyService implements InternalPolicyServiceAbstraction {
); );
}); });
} }
// --- End StateProvider methods
get$(policyType: PolicyType, policyFilter?: (policy: Policy) => boolean): Observable<Policy> {
return this.policies$.pipe(
concatMap(async (policies) => {
const userId = await this.stateService.getUserId();
const appliesToCurrentUser = await this.checkPoliciesThatApplyToUser(
policies,
policyType,
policyFilter,
userId,
);
if (appliesToCurrentUser) {
return policies.find((policy) => policy.type === policyType && policy.enabled);
}
}),
);
}
async getAll(type?: PolicyType, userId?: string): Promise<Policy[]> {
let response: Policy[] = [];
const decryptedPolicies = await this.stateService.getDecryptedPolicies({ userId: userId });
if (decryptedPolicies != null) {
response = decryptedPolicies;
} else {
const diskPolicies = await this.stateService.getEncryptedPolicies({ userId: userId });
for (const id in diskPolicies) {
if (Object.prototype.hasOwnProperty.call(diskPolicies, id)) {
response.push(new Policy(diskPolicies[id]));
}
}
await this.stateService.setDecryptedPolicies(response, { userId: userId });
}
if (type != null) {
return response.filter((policy) => policy.type === type);
} else {
return response;
}
}
masterPasswordPolicyOptions$(policies?: Policy[]): Observable<MasterPasswordPolicyOptions> { masterPasswordPolicyOptions$(policies?: Policy[]): Observable<MasterPasswordPolicyOptions> {
const observable = policies ? of(policies) : this.policies$; const observable = policies ? of(policies) : this.policies$;
@@ -205,15 +151,6 @@ export class PolicyService implements InternalPolicyServiceAbstraction {
); );
} }
policyAppliesToActiveUser$(policyType: PolicyType, policyFilter?: (policy: Policy) => boolean) {
return this.policies$.pipe(
concatMap(async (policies) => {
const userId = await this.stateService.getUserId();
return await this.checkPoliciesThatApplyToUser(policies, policyType, policyFilter, userId);
}),
);
}
evaluateMasterPassword( evaluateMasterPassword(
passwordStrength: number, passwordStrength: number,
newPassword: string, newPassword: string,
@@ -288,68 +225,20 @@ export class PolicyService implements InternalPolicyServiceAbstraction {
return policiesResponse.data.map((response) => this.mapPolicyFromResponse(response)); return policiesResponse.data.map((response) => this.mapPolicyFromResponse(response));
} }
async policyAppliesToUser( async upsert(policy: PolicyData): Promise<void> {
policyType: PolicyType, await this.activeUserPolicyState.update((policies) => {
policyFilter?: (policy: Policy) => boolean, policies ??= {};
userId?: string, policies[policy.id] = policy;
) { return policies;
const policies = await this.getAll(policyType, userId); });
return this.checkPoliciesThatApplyToUser(policies, policyType, policyFilter, userId);
}
async upsert(policy: PolicyData): Promise<any> {
let policies = await this.stateService.getEncryptedPolicies();
if (policies == null) {
policies = {};
}
policies[policy.id] = policy;
await this.updateObservables(policies);
await this.stateService.setDecryptedPolicies(null);
await this.stateService.setEncryptedPolicies(policies);
} }
async replace(policies: { [id: string]: PolicyData }): Promise<void> { async replace(policies: { [id: string]: PolicyData }): Promise<void> {
await this.updateObservables(policies); await this.activeUserPolicyState.update(() => policies);
await this.stateService.setDecryptedPolicies(null);
await this.stateService.setEncryptedPolicies(policies);
} }
async clear(userId?: string): Promise<void> { async clear(userId?: UserId): Promise<void> {
if (userId == null || userId == (await this.stateService.getUserId())) { await this.stateProvider.setUserState(POLICIES, null, userId);
this._policies.next([]);
}
await this.stateService.setDecryptedPolicies(null, { userId: userId });
await this.stateService.setEncryptedPolicies(null, { userId: userId });
}
private async updateObservables(policiesMap: { [id: string]: PolicyData }) {
const policies = Object.values(policiesMap || {}).map((f) => new Policy(f));
this._policies.next(policies);
}
private async checkPoliciesThatApplyToUser(
policies: Policy[],
policyType: PolicyType,
policyFilter?: (policy: Policy) => boolean,
userId?: string,
) {
const organizations = await this.organizationService.getAll(userId);
const filteredPolicies = policies.filter(
(p) => p.type === policyType && p.enabled && (policyFilter == null || policyFilter(p)),
);
const policySet = new Set(filteredPolicies.map((p) => p.organizationId));
return organizations.some(
(o) =>
o.status >= OrganizationUserStatusType.Accepted &&
o.usePolicies &&
policySet.has(o.id) &&
!this.isExemptFromPolicy(policyType, o),
);
} }
/** /**

View File

@@ -53,6 +53,10 @@ const INLINE_MENU_VISIBILITY = new KeyDefinition(
}, },
); );
const ENABLE_CONTEXT_MENU = new KeyDefinition(AUTOFILL_SETTINGS_DISK, "enableContextMenu", {
deserializer: (value: boolean) => value ?? true,
});
const CLEAR_CLIPBOARD_DELAY = new KeyDefinition( const CLEAR_CLIPBOARD_DELAY = new KeyDefinition(
AUTOFILL_SETTINGS_DISK_LOCAL, AUTOFILL_SETTINGS_DISK_LOCAL,
"clearClipboardDelay", "clearClipboardDelay",
@@ -75,6 +79,8 @@ export abstract class AutofillSettingsServiceAbstraction {
setAutoCopyTotp: (newValue: boolean) => Promise<void>; setAutoCopyTotp: (newValue: boolean) => Promise<void>;
inlineMenuVisibility$: Observable<InlineMenuVisibilitySetting>; inlineMenuVisibility$: Observable<InlineMenuVisibilitySetting>;
setInlineMenuVisibility: (newValue: InlineMenuVisibilitySetting) => Promise<void>; setInlineMenuVisibility: (newValue: InlineMenuVisibilitySetting) => Promise<void>;
enableContextMenu$: Observable<boolean>;
setEnableContextMenu: (newValue: boolean) => Promise<void>;
clearClipboardDelay$: Observable<ClearClipboardDelaySetting>; clearClipboardDelay$: Observable<ClearClipboardDelaySetting>;
setClearClipboardDelay: (newValue: ClearClipboardDelaySetting) => Promise<void>; setClearClipboardDelay: (newValue: ClearClipboardDelaySetting) => Promise<void>;
} }
@@ -100,6 +106,9 @@ export class AutofillSettingsService implements AutofillSettingsServiceAbstracti
private inlineMenuVisibilityState: GlobalState<InlineMenuVisibilitySetting>; private inlineMenuVisibilityState: GlobalState<InlineMenuVisibilitySetting>;
readonly inlineMenuVisibility$: Observable<InlineMenuVisibilitySetting>; readonly inlineMenuVisibility$: Observable<InlineMenuVisibilitySetting>;
private enableContextMenuState: GlobalState<boolean>;
readonly enableContextMenu$: Observable<boolean>;
private clearClipboardDelayState: ActiveUserState<ClearClipboardDelaySetting>; private clearClipboardDelayState: ActiveUserState<ClearClipboardDelaySetting>;
readonly clearClipboardDelay$: Observable<ClearClipboardDelaySetting>; readonly clearClipboardDelay$: Observable<ClearClipboardDelaySetting>;
@@ -142,6 +151,9 @@ export class AutofillSettingsService implements AutofillSettingsServiceAbstracti
map((x) => x ?? AutofillOverlayVisibility.Off), map((x) => x ?? AutofillOverlayVisibility.Off),
); );
this.enableContextMenuState = this.stateProvider.getGlobal(ENABLE_CONTEXT_MENU);
this.enableContextMenu$ = this.enableContextMenuState.state$.pipe(map((x) => x ?? true));
this.clearClipboardDelayState = this.stateProvider.getActive(CLEAR_CLIPBOARD_DELAY); this.clearClipboardDelayState = this.stateProvider.getActive(CLEAR_CLIPBOARD_DELAY);
this.clearClipboardDelay$ = this.clearClipboardDelayState.state$.pipe( this.clearClipboardDelay$ = this.clearClipboardDelayState.state$.pipe(
map((x) => x ?? ClearClipboardDelay.Never), map((x) => x ?? ClearClipboardDelay.Never),
@@ -172,6 +184,10 @@ export class AutofillSettingsService implements AutofillSettingsServiceAbstracti
await this.inlineMenuVisibilityState.update(() => newValue); await this.inlineMenuVisibilityState.update(() => newValue);
} }
async setEnableContextMenu(newValue: boolean): Promise<void> {
await this.enableContextMenuState.update(() => newValue);
}
async setClearClipboardDelay(newValue: ClearClipboardDelaySetting): Promise<void> { async setClearClipboardDelay(newValue: ClearClipboardDelaySetting): Promise<void> {
await this.clearClipboardDelayState.update(() => newValue); await this.clearClipboardDelayState.update(() => newValue);
} }

View File

@@ -1,8 +1,6 @@
import { Observable } from "rxjs"; import { Observable } from "rxjs";
import { OrganizationData } from "../../admin-console/models/data/organization.data"; import { OrganizationData } from "../../admin-console/models/data/organization.data";
import { PolicyData } from "../../admin-console/models/data/policy.data";
import { Policy } from "../../admin-console/models/domain/policy";
import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable"; import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable";
import { ForceSetPasswordReason } from "../../auth/models/domain/force-set-password-reason"; import { ForceSetPasswordReason } from "../../auth/models/domain/force-set-password-reason";
import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { KdfConfig } from "../../auth/models/domain/kdf-config";
@@ -181,14 +179,6 @@ export abstract class StateService<T extends Account = Account> {
* @deprecated For migration purposes only, use setDecryptedUserKeyPin instead * @deprecated For migration purposes only, use setDecryptedUserKeyPin instead
*/ */
setDecryptedPinProtected: (value: EncString, options?: StorageOptions) => Promise<void>; setDecryptedPinProtected: (value: EncString, options?: StorageOptions) => Promise<void>;
/**
* @deprecated Do not call this, use PolicyService
*/
getDecryptedPolicies: (options?: StorageOptions) => Promise<Policy[]>;
/**
* @deprecated Do not call this, use PolicyService
*/
setDecryptedPolicies: (value: Policy[], options?: StorageOptions) => Promise<void>;
/** /**
* @deprecated Do not call this directly, use SendService * @deprecated Do not call this directly, use SendService
*/ */
@@ -199,8 +189,6 @@ export abstract class StateService<T extends Account = Account> {
setDecryptedSends: (value: SendView[], options?: StorageOptions) => Promise<void>; setDecryptedSends: (value: SendView[], options?: StorageOptions) => Promise<void>;
getDefaultUriMatch: (options?: StorageOptions) => Promise<UriMatchType>; getDefaultUriMatch: (options?: StorageOptions) => Promise<UriMatchType>;
setDefaultUriMatch: (value: UriMatchType, options?: StorageOptions) => Promise<void>; setDefaultUriMatch: (value: UriMatchType, options?: StorageOptions) => Promise<void>;
getDisableContextMenuItem: (options?: StorageOptions) => Promise<boolean>;
setDisableContextMenuItem: (value: boolean, options?: StorageOptions) => Promise<void>;
/** /**
* @deprecated Do not call this, use SettingsService * @deprecated Do not call this, use SettingsService
*/ */
@@ -279,17 +267,6 @@ export abstract class StateService<T extends Account = Account> {
* @deprecated For migration purposes only, use setEncryptedUserKeyPin instead * @deprecated For migration purposes only, use setEncryptedUserKeyPin instead
*/ */
setEncryptedPinProtected: (value: string, options?: StorageOptions) => Promise<void>; setEncryptedPinProtected: (value: string, options?: StorageOptions) => Promise<void>;
/**
* @deprecated Do not call this directly, use PolicyService
*/
getEncryptedPolicies: (options?: StorageOptions) => Promise<{ [id: string]: PolicyData }>;
/**
* @deprecated Do not call this directly, use PolicyService
*/
setEncryptedPolicies: (
value: { [id: string]: PolicyData },
options?: StorageOptions,
) => Promise<void>;
/** /**
* @deprecated Do not call this directly, use SendService * @deprecated Do not call this directly, use SendService
*/ */

View File

@@ -12,6 +12,7 @@ import {
BIOMETRIC_UNLOCK_ENABLED, BIOMETRIC_UNLOCK_ENABLED,
DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT, DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT,
ENCRYPTED_CLIENT_KEY_HALF, ENCRYPTED_CLIENT_KEY_HALF,
FINGERPRINT_VALIDATED,
PROMPT_AUTOMATICALLY, PROMPT_AUTOMATICALLY,
PROMPT_CANCELLED, PROMPT_CANCELLED,
REQUIRE_PASSWORD_ON_START, REQUIRE_PASSWORD_ON_START,
@@ -67,6 +68,19 @@ describe("BiometricStateService", () => {
}); });
}); });
describe("fingerprintValidated$", () => {
it("emits when the fingerprint validated state changes", async () => {
const state = stateProvider.global.getFake(FINGERPRINT_VALIDATED);
state.stateSubject.next(undefined);
expect(await firstValueFrom(sut.fingerprintValidated$)).toBe(false);
state.stateSubject.next(true);
expect(await firstValueFrom(sut.fingerprintValidated$)).toEqual(true);
});
});
describe("setEncryptedClientKeyHalf", () => { describe("setEncryptedClientKeyHalf", () => {
it("updates encryptedClientKeyHalf$", async () => { it("updates encryptedClientKeyHalf$", async () => {
await sut.setEncryptedClientKeyHalf(encClientKeyHalf); await sut.setEncryptedClientKeyHalf(encClientKeyHalf);
@@ -207,4 +221,20 @@ describe("BiometricStateService", () => {
expect(await sut.getBiometricUnlockEnabled(userId)).toBe(false); expect(await sut.getBiometricUnlockEnabled(userId)).toBe(false);
}); });
}); });
describe("setFingerprintValidated", () => {
it("updates fingerprintValidated$", async () => {
await sut.setFingerprintValidated(true);
expect(await firstValueFrom(sut.fingerprintValidated$)).toBe(true);
});
it("updates state", async () => {
await sut.setFingerprintValidated(true);
expect(stateProvider.global.getFake(FINGERPRINT_VALIDATED).nextMock).toHaveBeenCalledWith(
true,
);
});
});
}); });

View File

@@ -2,7 +2,7 @@ import { Observable, firstValueFrom, map } from "rxjs";
import { UserId } from "../../types/guid"; import { UserId } from "../../types/guid";
import { EncryptedString, EncString } from "../models/domain/enc-string"; import { EncryptedString, EncString } from "../models/domain/enc-string";
import { ActiveUserState, StateProvider } from "../state"; import { ActiveUserState, GlobalState, StateProvider } from "../state";
import { import {
BIOMETRIC_UNLOCK_ENABLED, BIOMETRIC_UNLOCK_ENABLED,
@@ -11,6 +11,7 @@ import {
DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT, DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT,
PROMPT_AUTOMATICALLY, PROMPT_AUTOMATICALLY,
PROMPT_CANCELLED, PROMPT_CANCELLED,
FINGERPRINT_VALIDATED,
} from "./biometric.state"; } from "./biometric.state";
export abstract class BiometricStateService { export abstract class BiometricStateService {
@@ -49,6 +50,10 @@ export abstract class BiometricStateService {
* tracks the currently active user * tracks the currently active user
*/ */
promptAutomatically$: Observable<boolean>; promptAutomatically$: Observable<boolean>;
/**
* Whether or not IPC fingerprint has been validated by the user this session.
*/
fingerprintValidated$: Observable<boolean>;
/** /**
* Updates the require password on start state for the currently active user. * Updates the require password on start state for the currently active user.
@@ -88,6 +93,11 @@ export abstract class BiometricStateService {
* @param prompt Whether or not to prompt for biometrics on application start. * @param prompt Whether or not to prompt for biometrics on application start.
*/ */
abstract setPromptAutomatically(prompt: boolean): Promise<void>; abstract setPromptAutomatically(prompt: boolean): Promise<void>;
/**
* Updates whether or not IPC has been validated by the user this session
* @param validated the value to save
*/
abstract setFingerprintValidated(validated: boolean): Promise<void>;
abstract logout(userId: UserId): Promise<void>; abstract logout(userId: UserId): Promise<void>;
} }
@@ -99,12 +109,14 @@ export class DefaultBiometricStateService implements BiometricStateService {
private dismissedRequirePasswordOnStartCalloutState: ActiveUserState<boolean>; private dismissedRequirePasswordOnStartCalloutState: ActiveUserState<boolean>;
private promptCancelledState: ActiveUserState<boolean>; private promptCancelledState: ActiveUserState<boolean>;
private promptAutomaticallyState: ActiveUserState<boolean>; private promptAutomaticallyState: ActiveUserState<boolean>;
private fingerprintValidatedState: GlobalState<boolean>;
biometricUnlockEnabled$: Observable<boolean>; biometricUnlockEnabled$: Observable<boolean>;
encryptedClientKeyHalf$: Observable<EncString | undefined>; encryptedClientKeyHalf$: Observable<EncString | undefined>;
requirePasswordOnStart$: Observable<boolean>; requirePasswordOnStart$: Observable<boolean>;
dismissedRequirePasswordOnStartCallout$: Observable<boolean>; dismissedRequirePasswordOnStartCallout$: Observable<boolean>;
promptCancelled$: Observable<boolean>; promptCancelled$: Observable<boolean>;
promptAutomatically$: Observable<boolean>; promptAutomatically$: Observable<boolean>;
fingerprintValidated$: Observable<boolean>;
constructor(private stateProvider: StateProvider) { constructor(private stateProvider: StateProvider) {
this.biometricUnlockEnabledState = this.stateProvider.getActive(BIOMETRIC_UNLOCK_ENABLED); this.biometricUnlockEnabledState = this.stateProvider.getActive(BIOMETRIC_UNLOCK_ENABLED);
@@ -130,6 +142,9 @@ export class DefaultBiometricStateService implements BiometricStateService {
this.promptCancelled$ = this.promptCancelledState.state$.pipe(map(Boolean)); this.promptCancelled$ = this.promptCancelledState.state$.pipe(map(Boolean));
this.promptAutomaticallyState = this.stateProvider.getActive(PROMPT_AUTOMATICALLY); this.promptAutomaticallyState = this.stateProvider.getActive(PROMPT_AUTOMATICALLY);
this.promptAutomatically$ = this.promptAutomaticallyState.state$.pipe(map(Boolean)); this.promptAutomatically$ = this.promptAutomaticallyState.state$.pipe(map(Boolean));
this.fingerprintValidatedState = this.stateProvider.getGlobal(FINGERPRINT_VALIDATED);
this.fingerprintValidated$ = this.fingerprintValidatedState.state$.pipe(map(Boolean));
} }
async setBiometricUnlockEnabled(enabled: boolean): Promise<void> { async setBiometricUnlockEnabled(enabled: boolean): Promise<void> {
@@ -207,6 +222,10 @@ export class DefaultBiometricStateService implements BiometricStateService {
async setPromptAutomatically(prompt: boolean): Promise<void> { async setPromptAutomatically(prompt: boolean): Promise<void> {
await this.promptAutomaticallyState.update(() => prompt); await this.promptAutomaticallyState.update(() => prompt);
} }
async setFingerprintValidated(validated: boolean): Promise<void> {
await this.fingerprintValidatedState.update(() => validated);
}
} }
function encryptedClientKeyHalfToEncString( function encryptedClientKeyHalfToEncString(

View File

@@ -5,6 +5,7 @@ import {
BIOMETRIC_UNLOCK_ENABLED, BIOMETRIC_UNLOCK_ENABLED,
DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT, DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT,
ENCRYPTED_CLIENT_KEY_HALF, ENCRYPTED_CLIENT_KEY_HALF,
FINGERPRINT_VALIDATED,
PROMPT_AUTOMATICALLY, PROMPT_AUTOMATICALLY,
PROMPT_CANCELLED, PROMPT_CANCELLED,
REQUIRE_PASSWORD_ON_START, REQUIRE_PASSWORD_ON_START,
@@ -16,7 +17,8 @@ describe.each([
[PROMPT_CANCELLED, true], [PROMPT_CANCELLED, true],
[PROMPT_AUTOMATICALLY, true], [PROMPT_AUTOMATICALLY, true],
[REQUIRE_PASSWORD_ON_START, true], [REQUIRE_PASSWORD_ON_START, true],
[BIOMETRIC_UNLOCK_ENABLED, "test"], [BIOMETRIC_UNLOCK_ENABLED, true],
[FINGERPRINT_VALIDATED, true],
])( ])(
"deserializes state %s", "deserializes state %s",
( (

View File

@@ -74,3 +74,14 @@ export const PROMPT_AUTOMATICALLY = new KeyDefinition<boolean>(
deserializer: (obj) => obj, deserializer: (obj) => obj,
}, },
); );
/**
* Stores whether or not IPC handshake has been validated this session.
*/
export const FINGERPRINT_VALIDATED = new KeyDefinition<boolean>(
BIOMETRIC_SETTINGS_DISK,
"fingerprintValidated",
{
deserializer: (obj) => obj,
},
);

View File

@@ -1,8 +1,6 @@
import { Jsonify } from "type-fest"; import { Jsonify } from "type-fest";
import { OrganizationData } from "../../../admin-console/models/data/organization.data"; import { OrganizationData } from "../../../admin-console/models/data/organization.data";
import { PolicyData } from "../../../admin-console/models/data/policy.data";
import { Policy } from "../../../admin-console/models/domain/policy";
import { AdminAuthRequestStorable } from "../../../auth/models/domain/admin-auth-req-storable"; import { AdminAuthRequestStorable } from "../../../auth/models/domain/admin-auth-req-storable";
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason"; import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
import { KeyConnectorUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/key-connector-user-decryption-option"; import { KeyConnectorUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/key-connector-user-decryption-option";
@@ -87,7 +85,6 @@ export class AccountData {
>(); >();
localData?: any; localData?: any;
sends?: DataEncryptionPair<SendData, SendView> = new DataEncryptionPair<SendData, SendView>(); sends?: DataEncryptionPair<SendData, SendView> = new DataEncryptionPair<SendData, SendView>();
policies?: DataEncryptionPair<PolicyData, Policy> = new DataEncryptionPair<PolicyData, Policy>();
passwordGenerationHistory?: EncryptionPair< passwordGenerationHistory?: EncryptionPair<
GeneratedPasswordHistory[], GeneratedPasswordHistory[],
GeneratedPasswordHistory[] GeneratedPasswordHistory[]

View File

@@ -26,6 +26,5 @@ export class GlobalState {
enableBrowserIntegrationFingerprint?: boolean; enableBrowserIntegrationFingerprint?: boolean;
enableDuckDuckGoBrowserIntegration?: boolean; enableDuckDuckGoBrowserIntegration?: boolean;
neverDomains?: { [id: string]: unknown }; neverDomains?: { [id: string]: unknown };
disableContextMenuItem?: boolean;
deepLinkRedirectUrl?: string; deepLinkRedirectUrl?: string;
} }

View File

@@ -2,8 +2,6 @@ import { BehaviorSubject, Observable, map } from "rxjs";
import { Jsonify, JsonValue } from "type-fest"; import { Jsonify, JsonValue } from "type-fest";
import { OrganizationData } from "../../admin-console/models/data/organization.data"; import { OrganizationData } from "../../admin-console/models/data/organization.data";
import { PolicyData } from "../../admin-console/models/data/policy.data";
import { Policy } from "../../admin-console/models/domain/policy";
import { AccountService } from "../../auth/abstractions/account.service"; import { AccountService } from "../../auth/abstractions/account.service";
import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { AuthenticationStatus } from "../../auth/enums/authentication-status";
import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable"; import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable";
@@ -799,24 +797,6 @@ export class StateService<
); );
} }
@withPrototypeForArrayMembers(Policy)
async getDecryptedPolicies(options?: StorageOptions): Promise<Policy[]> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
)?.data?.policies?.decrypted;
}
async setDecryptedPolicies(value: Policy[], options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
);
account.data.policies.decrypted = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
);
}
@withPrototypeForArrayMembers(SendView) @withPrototypeForArrayMembers(SendView)
async getDecryptedSends(options?: StorageOptions): Promise<SendView[]> { async getDecryptedSends(options?: StorageOptions): Promise<SendView[]> {
return ( return (
@@ -852,24 +832,6 @@ export class StateService<
); );
} }
async getDisableContextMenuItem(options?: StorageOptions): Promise<boolean> {
return (
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
?.disableContextMenuItem ?? false
);
}
async setDisableContextMenuItem(value: boolean, options?: StorageOptions): Promise<void> {
const globals = await this.getGlobals(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
globals.disableContextMenuItem = value;
await this.saveGlobals(
globals,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getDisableFavicon(options?: StorageOptions): Promise<boolean> { async getDisableFavicon(options?: StorageOptions): Promise<boolean> {
return ( return (
( (
@@ -1350,27 +1312,6 @@ export class StateService<
); );
} }
@withPrototypeForObjectValues(PolicyData)
async getEncryptedPolicies(options?: StorageOptions): Promise<{ [id: string]: PolicyData }> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.data?.policies?.encrypted;
}
async setEncryptedPolicies(
value: { [id: string]: PolicyData },
options?: StorageOptions,
): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
account.data.policies.encrypted = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
@withPrototypeForObjectValues(SendData) @withPrototypeForObjectValues(SendData)
async getEncryptedSends(options?: StorageOptions): Promise<{ [id: string]: SendData }> { async getEncryptedSends(options?: StorageOptions): Promise<{ [id: string]: SendData }> {
return ( return (

View File

@@ -9,6 +9,7 @@ import { MessagingService } from "../abstractions/messaging.service";
import { PlatformUtilsService } from "../abstractions/platform-utils.service"; import { PlatformUtilsService } from "../abstractions/platform-utils.service";
import { StateService } from "../abstractions/state.service"; import { StateService } from "../abstractions/state.service";
import { SystemService as SystemServiceAbstraction } from "../abstractions/system.service"; import { SystemService as SystemServiceAbstraction } from "../abstractions/system.service";
import { BiometricStateService } from "../biometrics/biometric-state.service";
import { Utils } from "../misc/utils"; import { Utils } from "../misc/utils";
export class SystemService implements SystemServiceAbstraction { export class SystemService implements SystemServiceAbstraction {
@@ -23,6 +24,7 @@ export class SystemService implements SystemServiceAbstraction {
private stateService: StateService, private stateService: StateService,
private autofillSettingsService: AutofillSettingsServiceAbstraction, private autofillSettingsService: AutofillSettingsServiceAbstraction,
private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
private biometricStateService: BiometricStateService,
) {} ) {}
async startProcessReload(authService: AuthService): Promise<void> { async startProcessReload(authService: AuthService): Promise<void> {
@@ -54,8 +56,9 @@ export class SystemService implements SystemServiceAbstraction {
} }
private async executeProcessReload() { private async executeProcessReload() {
const biometricLockedFingerprintValidated = const biometricLockedFingerprintValidated = await firstValueFrom(
await this.stateService.getBiometricFingerprintValidated(); this.biometricStateService.fingerprintValidated$,
);
if (!biometricLockedFingerprintValidated) { if (!biometricLockedFingerprintValidated) {
clearInterval(this.reloadInterval); clearInterval(this.reloadInterval);
this.reloadInterval = null; this.reloadInterval = null;

View File

@@ -110,9 +110,8 @@ describe("VaultTimeoutSettingsService", () => {
stateService.getAccountDecryptionOptions.mockResolvedValue( stateService.getAccountDecryptionOptions.mockResolvedValue(
new AccountDecryptionOptions({ hasMasterPassword: true }), new AccountDecryptionOptions({ hasMasterPassword: true }),
); );
policyService.policyAppliesToUser.mockResolvedValue(policy === null ? false : true); policyService.getAll$.mockReturnValue(
policyService.getAll.mockResolvedValue( of(policy === null ? [] : ([{ data: { action: policy } }] as unknown as Policy[])),
policy === null ? [] : ([{ data: { action: policy } }] as unknown as Policy[]),
); );
stateService.getVaultTimeoutAction.mockResolvedValue(userPreference); stateService.getVaultTimeoutAction.mockResolvedValue(userPreference);
@@ -140,9 +139,8 @@ describe("VaultTimeoutSettingsService", () => {
stateService.getAccountDecryptionOptions.mockResolvedValue( stateService.getAccountDecryptionOptions.mockResolvedValue(
new AccountDecryptionOptions({ hasMasterPassword: false }), new AccountDecryptionOptions({ hasMasterPassword: false }),
); );
policyService.policyAppliesToUser.mockResolvedValue(policy === null ? false : true); policyService.getAll$.mockReturnValue(
policyService.getAll.mockResolvedValue( of(policy === null ? [] : ([{ data: { action: policy } }] as unknown as Policy[])),
policy === null ? [] : ([{ data: { action: policy } }] as unknown as Policy[]),
); );
stateService.getVaultTimeoutAction.mockResolvedValue(userPreference); stateService.getVaultTimeoutAction.mockResolvedValue(userPreference);

View File

@@ -84,18 +84,18 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
return await biometricUnlockPromise; return await biometricUnlockPromise;
} }
async getVaultTimeout(userId?: string): Promise<number> { async getVaultTimeout(userId?: UserId): Promise<number> {
const vaultTimeout = await this.stateService.getVaultTimeout({ userId }); const vaultTimeout = await this.stateService.getVaultTimeout({ userId });
const policies = await firstValueFrom(
this.policyService.getAll$(PolicyType.MaximumVaultTimeout, userId),
);
if ( if (policies?.length) {
await this.policyService.policyAppliesToUser(PolicyType.MaximumVaultTimeout, null, userId)
) {
const policy = await this.policyService.getAll(PolicyType.MaximumVaultTimeout, userId);
// Remove negative values, and ensure it's smaller than maximum allowed value according to policy // Remove negative values, and ensure it's smaller than maximum allowed value according to policy
let timeout = Math.min(vaultTimeout, policy[0].data.minutes); let timeout = Math.min(vaultTimeout, policies[0].data.minutes);
if (vaultTimeout == null || timeout < 0) { if (vaultTimeout == null || timeout < 0) {
timeout = policy[0].data.minutes; timeout = policies[0].data.minutes;
} }
// TODO @jlf0dev: Can we move this somwhere else? Maybe add it to the initialization process? // TODO @jlf0dev: Can we move this somwhere else? Maybe add it to the initialization process?
@@ -111,23 +111,23 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
return vaultTimeout; return vaultTimeout;
} }
vaultTimeoutAction$(userId?: string) { vaultTimeoutAction$(userId?: UserId) {
return defer(() => this.getVaultTimeoutAction(userId)); return defer(() => this.getVaultTimeoutAction(userId));
} }
async getVaultTimeoutAction(userId?: string): Promise<VaultTimeoutAction> { async getVaultTimeoutAction(userId?: UserId): Promise<VaultTimeoutAction> {
const availableActions = await this.getAvailableVaultTimeoutActions(); const availableActions = await this.getAvailableVaultTimeoutActions();
if (availableActions.length === 1) { if (availableActions.length === 1) {
return availableActions[0]; return availableActions[0];
} }
const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction({ userId: userId }); const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction({ userId: userId });
const policies = await firstValueFrom(
this.policyService.getAll$(PolicyType.MaximumVaultTimeout, userId),
);
if ( if (policies?.length) {
await this.policyService.policyAppliesToUser(PolicyType.MaximumVaultTimeout, null, userId) const action = policies[0].data.action;
) {
const policy = await this.policyService.getAll(PolicyType.MaximumVaultTimeout, userId);
const action = policy[0].data.action;
// We really shouldn't need to set the value here, but multiple services relies on this value being correct. // We really shouldn't need to set the value here, but multiple services relies on this value being correct.
if (action && vaultTimeoutAction !== action) { if (action && vaultTimeoutAction !== action) {
await this.stateService.setVaultTimeoutAction(action, { userId: userId }); await this.stateService.setVaultTimeoutAction(action, { userId: userId });

View File

@@ -25,6 +25,8 @@ import { BadgeSettingsMigrator } from "./migrations/27-move-badge-settings-to-st
import { MoveBiometricUnlockToStateProviders } from "./migrations/28-move-biometric-unlock-to-state-providers"; import { MoveBiometricUnlockToStateProviders } from "./migrations/28-move-biometric-unlock-to-state-providers";
import { UserNotificationSettingsKeyMigrator } from "./migrations/29-move-user-notification-settings-to-state-provider"; import { UserNotificationSettingsKeyMigrator } from "./migrations/29-move-user-notification-settings-to-state-provider";
import { FixPremiumMigrator } from "./migrations/3-fix-premium"; import { FixPremiumMigrator } from "./migrations/3-fix-premium";
import { PolicyMigrator } from "./migrations/30-move-policy-state-to-state-provider";
import { EnableContextMenuMigrator } from "./migrations/31-move-enable-context-menu-to-autofill-settings-state-provider";
import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been-unlocked"; import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been-unlocked";
import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys"; import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys";
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key"; import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
@@ -34,7 +36,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting
import { MinVersionMigrator } from "./migrations/min-version"; import { MinVersionMigrator } from "./migrations/min-version";
export const MIN_VERSION = 2; export const MIN_VERSION = 2;
export const CURRENT_VERSION = 29; export const CURRENT_VERSION = 31;
export type MinVersion = typeof MIN_VERSION; export type MinVersion = typeof MIN_VERSION;
export function createMigrationBuilder() { export function createMigrationBuilder() {
@@ -66,7 +68,9 @@ export function createMigrationBuilder() {
.with(RevertLastSyncMigrator, 25, 26) .with(RevertLastSyncMigrator, 25, 26)
.with(BadgeSettingsMigrator, 26, 27) .with(BadgeSettingsMigrator, 26, 27)
.with(MoveBiometricUnlockToStateProviders, 27, 28) .with(MoveBiometricUnlockToStateProviders, 27, 28)
.with(UserNotificationSettingsKeyMigrator, 28, CURRENT_VERSION); .with(UserNotificationSettingsKeyMigrator, 28, 29)
.with(PolicyMigrator, 29, 30)
.with(EnableContextMenuMigrator, 30, CURRENT_VERSION);
} }
export async function currentVersion( export async function currentVersion(
@@ -99,8 +103,12 @@ export async function waitForMigrations(
const isReady = async () => { const isReady = async () => {
const version = await currentVersion(storageService, logService); const version = await currentVersion(storageService, logService);
// The saved version is what we consider the latest // The saved version is what we consider the latest
// migrations should be complete // migrations should be complete, the state version
return version === CURRENT_VERSION; // shouldn't become larger than `CURRENT_VERSION` in
// any normal usage of the application but it is common
// enough in dev scenarios where we want to consider that
// ready as well and return true in that scenario.
return version >= CURRENT_VERSION;
}; };
const wait = async (time: number) => { const wait = async (time: number) => {

View File

@@ -0,0 +1,192 @@
import { MockProxy, any } from "jest-mock-extended";
import { MigrationHelper } from "../migration-helper";
import { mockMigrationHelper } from "../migration-helper.spec";
import { PolicyMigrator } from "./30-move-policy-state-to-state-provider";
function exampleJSON() {
return {
global: {
otherStuff: "otherStuff1",
},
authenticatedAccounts: ["user-1", "user-2"],
"user-1": {
data: {
policies: {
encrypted: {
"policy-1": {
id: "policy-1",
organizationId: "fe1ff6ef-d2d4-49f3-9c07-b0c7013998f9",
type: 9, // max vault timeout
enabled: true,
data: {
hours: 1,
minutes: 30,
action: "lock",
},
},
"policy-2": {
id: "policy-2",
organizationId: "5f277723-6391-4b5c-add9-b0c200ee6967",
type: 3, // single org
enabled: true,
},
},
},
otherStuff: "otherStuff2",
},
otherStuff: "otherStuff3",
},
"user-2": {
data: {
otherStuff: "otherStuff4",
},
otherStuff: "otherStuff5",
},
};
}
function rollbackJSON() {
return {
"user_user-1_policies_policies": {
"policy-1": {
id: "policy-1",
organizationId: "fe1ff6ef-d2d4-49f3-9c07-b0c7013998f9",
type: 9,
enabled: true,
data: {
hours: 1,
minutes: 30,
action: "lock",
},
},
"policy-2": {
id: "policy-2",
organizationId: "5f277723-6391-4b5c-add9-b0c200ee6967",
type: 3,
enabled: true,
},
},
"user_user-2_policies_policies": null as any,
global: {
otherStuff: "otherStuff1",
},
authenticatedAccounts: ["user-1", "user-2"],
"user-1": {
data: {
otherStuff: "otherStuff2",
},
otherStuff: "otherStuff3",
},
"user-2": {
data: {
otherStuff: "otherStuff4",
},
otherStuff: "otherStuff5",
},
};
}
describe("PoliciesMigrator", () => {
let helper: MockProxy<MigrationHelper>;
let sut: PolicyMigrator;
const keyDefinitionLike = {
key: "policies",
stateDefinition: {
name: "policies",
},
};
describe("migrate", () => {
beforeEach(() => {
helper = mockMigrationHelper(exampleJSON(), 22);
sut = new PolicyMigrator(29, 30);
});
it("should remove policies from all old accounts", async () => {
await sut.migrate(helper);
expect(helper.set).toHaveBeenCalledWith("user-1", {
data: {
otherStuff: "otherStuff2",
},
otherStuff: "otherStuff3",
});
});
it("should set policies value in StateProvider framework for each account", async () => {
await sut.migrate(helper);
expect(helper.setToUser).toHaveBeenCalledWith("user-1", keyDefinitionLike, {
"policy-1": {
id: "policy-1",
organizationId: "fe1ff6ef-d2d4-49f3-9c07-b0c7013998f9",
type: 9,
enabled: true,
data: {
hours: 1,
minutes: 30,
action: "lock",
},
},
"policy-2": {
id: "policy-2",
organizationId: "5f277723-6391-4b5c-add9-b0c200ee6967",
type: 3,
enabled: true,
},
});
});
});
describe("rollback", () => {
beforeEach(() => {
helper = mockMigrationHelper(rollbackJSON(), 23);
sut = new PolicyMigrator(29, 30);
});
it.each(["user-1", "user-2"])("should null out new values", async (userId) => {
await sut.rollback(helper);
expect(helper.setToUser).toHaveBeenCalledWith(userId, keyDefinitionLike, null);
});
it("should add policy values back to accounts", async () => {
await sut.rollback(helper);
expect(helper.set).toHaveBeenCalled();
expect(helper.set).toHaveBeenCalledWith("user-1", {
data: {
policies: {
encrypted: {
"policy-1": {
id: "policy-1",
organizationId: "fe1ff6ef-d2d4-49f3-9c07-b0c7013998f9",
type: 9,
enabled: true,
data: {
hours: 1,
minutes: 30,
action: "lock",
},
},
"policy-2": {
id: "policy-2",
organizationId: "5f277723-6391-4b5c-add9-b0c200ee6967",
type: 3,
enabled: true,
},
},
},
otherStuff: "otherStuff2",
},
otherStuff: "otherStuff3",
});
});
it("should not try to restore values to missing accounts", async () => {
await sut.rollback(helper);
expect(helper.set).not.toHaveBeenCalledWith("user-3", any());
});
});
});

View File

@@ -0,0 +1,76 @@
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
import { Migrator } from "../migrator";
enum PolicyType {
TwoFactorAuthentication = 0, // Requires users to have 2fa enabled
MasterPassword = 1, // Sets minimum requirements for master password complexity
PasswordGenerator = 2, // Sets minimum requirements/default type for generated passwords/passphrases
SingleOrg = 3, // Allows users to only be apart of one organization
RequireSso = 4, // Requires users to authenticate with SSO
PersonalOwnership = 5, // Disables personal vault ownership for adding/cloning items
DisableSend = 6, // Disables the ability to create and edit Bitwarden Sends
SendOptions = 7, // Sets restrictions or defaults for Bitwarden Sends
ResetPassword = 8, // Allows orgs to use reset password : also can enable auto-enrollment during invite flow
MaximumVaultTimeout = 9, // Sets the maximum allowed vault timeout
DisablePersonalVaultExport = 10, // Disable personal vault export
ActivateAutofill = 11, // Activates autofill with page load on the browser extension
}
type PolicyDataType = {
id: string;
organizationId: string;
type: PolicyType;
data: Record<string, string | number | boolean>;
enabled: boolean;
};
type ExpectedAccountType = {
data?: {
policies?: {
encrypted?: Record<string, PolicyDataType>;
};
};
};
const POLICIES_KEY: KeyDefinitionLike = {
key: "policies",
stateDefinition: {
name: "policies",
},
};
export class PolicyMigrator extends Migrator<29, 30> {
async migrate(helper: MigrationHelper): Promise<void> {
const accounts = await helper.getAccounts<ExpectedAccountType>();
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
const value = account?.data?.policies?.encrypted;
if (value != null) {
await helper.setToUser(userId, POLICIES_KEY, value);
delete account.data.policies;
await helper.set(userId, account);
}
}
await Promise.all(accounts.map(({ userId, account }) => migrateAccount(userId, account)));
}
async rollback(helper: MigrationHelper): Promise<void> {
const accounts = await helper.getAccounts<ExpectedAccountType>();
async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> {
const value = await helper.getFromUser(userId, POLICIES_KEY);
if (account) {
account.data = Object.assign(account.data ?? {}, {
policies: {
encrypted: value,
},
});
await helper.set(userId, account);
}
await helper.setToUser(userId, POLICIES_KEY, null);
}
await Promise.all(accounts.map(({ userId, account }) => rollbackAccount(userId, account)));
}
}

View File

@@ -0,0 +1,91 @@
import { any, MockProxy } from "jest-mock-extended";
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
import { mockMigrationHelper } from "../migration-helper.spec";
import { EnableContextMenuMigrator } from "./31-move-enable-context-menu-to-autofill-settings-state-provider";
function exampleJSON() {
return {
global: {
disableContextMenuItem: true,
otherStuff: "otherStuff1",
},
otherStuff: "otherStuff2",
};
}
function rollbackJSON() {
return {
global_autofillSettings_enableContextMenu: false,
global: {
otherStuff: "otherStuff1",
},
otherStuff: "otherStuff2",
};
}
const enableContextMenuKeyDefinition: KeyDefinitionLike = {
stateDefinition: {
name: "autofillSettings",
},
key: "enableContextMenu",
};
describe("EnableContextMenuMigrator", () => {
let helper: MockProxy<MigrationHelper>;
let sut: EnableContextMenuMigrator;
describe("migrate", () => {
beforeEach(() => {
helper = mockMigrationHelper(exampleJSON(), 30);
sut = new EnableContextMenuMigrator(30, 31);
});
it("should remove global disableContextMenuItem setting", async () => {
await sut.migrate(helper);
expect(helper.set).toHaveBeenCalledTimes(1);
expect(helper.set).toHaveBeenCalledWith("global", {
otherStuff: "otherStuff1",
});
});
it("should set enableContextMenu globally", async () => {
await sut.migrate(helper);
expect(helper.setToGlobal).toHaveBeenCalledTimes(1);
expect(helper.setToGlobal).toHaveBeenCalledWith(enableContextMenuKeyDefinition, false);
});
});
describe("rollback", () => {
beforeEach(() => {
helper = mockMigrationHelper(rollbackJSON(), 31);
sut = new EnableContextMenuMigrator(30, 31);
});
it("should null out new enableContextMenu global value", async () => {
await sut.rollback(helper);
expect(helper.setToGlobal).toHaveBeenCalledTimes(1);
expect(helper.setToGlobal).toHaveBeenCalledWith(enableContextMenuKeyDefinition, null);
});
it("should add disableContextMenuItem global value back", async () => {
await sut.rollback(helper);
expect(helper.set).toHaveBeenCalledTimes(1);
expect(helper.set).toHaveBeenCalledWith("global", {
disableContextMenuItem: true,
otherStuff: "otherStuff1",
});
});
it("should not try to restore values to missing accounts", async () => {
await sut.rollback(helper);
expect(helper.set).not.toHaveBeenCalledWith("user-3", any());
});
});
});

View File

@@ -0,0 +1,46 @@
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
import { Migrator } from "../migrator";
type ExpectedGlobalState = {
disableContextMenuItem?: boolean;
};
const enableContextMenuKeyDefinition: KeyDefinitionLike = {
stateDefinition: {
name: "autofillSettings",
},
key: "enableContextMenu",
};
export class EnableContextMenuMigrator extends Migrator<30, 31> {
async migrate(helper: MigrationHelper): Promise<void> {
const globalState = await helper.get<ExpectedGlobalState>("global");
// disableContextMenuItem -> enableContextMenu
if (globalState?.disableContextMenuItem != null) {
await helper.setToGlobal(enableContextMenuKeyDefinition, !globalState.disableContextMenuItem);
// delete `disableContextMenuItem` from state global
delete globalState.disableContextMenuItem;
await helper.set<ExpectedGlobalState>("global", globalState);
}
}
async rollback(helper: MigrationHelper): Promise<void> {
const globalState = (await helper.get<ExpectedGlobalState>("global")) || {};
const enableContextMenu: boolean = await helper.getFromGlobal(enableContextMenuKeyDefinition);
// enableContextMenu -> disableContextMenuItem
if (enableContextMenu != null) {
await helper.set<ExpectedGlobalState>("global", {
...globalState,
disableContextMenuItem: !enableContextMenu,
});
// remove the global state provider framework key for `enableContextMenu`
await helper.setToGlobal(enableContextMenuKeyDefinition, null);
}
}
}

532
package-lock.json generated
View File

@@ -67,7 +67,7 @@
"qrious": "4.0.2", "qrious": "4.0.2",
"rxjs": "7.8.1", "rxjs": "7.8.1",
"tabbable": "6.2.0", "tabbable": "6.2.0",
"tldts": "6.1.11", "tldts": "6.1.13",
"utf-8-validate": "6.0.3", "utf-8-validate": "6.0.3",
"zone.js": "0.13.3", "zone.js": "0.13.3",
"zxcvbn": "4.4.2" "zxcvbn": "4.4.2"
@@ -96,7 +96,7 @@
"@types/argon2-browser": "1.18.1", "@types/argon2-browser": "1.18.1",
"@types/chrome": "0.0.262", "@types/chrome": "0.0.262",
"@types/duo_web_sdk": "2.7.1", "@types/duo_web_sdk": "2.7.1",
"@types/firefox-webext-browser": "111.0.1", "@types/firefox-webext-browser": "111.0.5",
"@types/inquirer": "8.2.10", "@types/inquirer": "8.2.10",
"@types/jest": "29.5.12", "@types/jest": "29.5.12",
"@types/jquery": "3.5.29", "@types/jquery": "3.5.29",
@@ -129,19 +129,19 @@
"cross-env": "7.0.3", "cross-env": "7.0.3",
"css-loader": "6.8.1", "css-loader": "6.8.1",
"electron": "28.2.6", "electron": "28.2.6",
"electron-builder": "24.9.1", "electron-builder": "24.13.3",
"electron-log": "5.0.1", "electron-log": "5.0.1",
"electron-reload": "2.0.0-alpha.1", "electron-reload": "2.0.0-alpha.1",
"electron-store": "8.1.0", "electron-store": "8.1.0",
"electron-updater": "6.1.8", "electron-updater": "6.1.8",
"eslint": "8.56.0", "eslint": "8.57.0",
"eslint-config-prettier": "9.1.0", "eslint-config-prettier": "9.1.0",
"eslint-import-resolver-typescript": "3.6.1", "eslint-import-resolver-typescript": "3.6.1",
"eslint-plugin-import": "2.29.1", "eslint-plugin-import": "2.29.1",
"eslint-plugin-rxjs": "5.0.3", "eslint-plugin-rxjs": "5.0.3",
"eslint-plugin-rxjs-angular": "2.0.1", "eslint-plugin-rxjs-angular": "2.0.1",
"eslint-plugin-storybook": "0.8.0", "eslint-plugin-storybook": "0.8.0",
"eslint-plugin-tailwindcss": "3.13.1", "eslint-plugin-tailwindcss": "3.14.3",
"gulp": "4.0.2", "gulp": "4.0.2",
"gulp-filter": "9.0.1", "gulp-filter": "9.0.1",
"gulp-if": "3.0.0", "gulp-if": "3.0.0",
@@ -172,7 +172,7 @@
"sass": "1.69.5", "sass": "1.69.5",
"sass-loader": "13.3.3", "sass-loader": "13.3.3",
"storybook": "7.6.17", "storybook": "7.6.17",
"style-loader": "3.3.3", "style-loader": "3.3.4",
"tailwindcss": "3.4.1", "tailwindcss": "3.4.1",
"ts-jest": "29.1.2", "ts-jest": "29.1.2",
"ts-loader": "9.5.1", "ts-loader": "9.5.1",
@@ -225,7 +225,7 @@
"papaparse": "5.4.1", "papaparse": "5.4.1",
"proper-lockfile": "4.1.2", "proper-lockfile": "4.1.2",
"rxjs": "7.8.1", "rxjs": "7.8.1",
"tldts": "6.1.11", "tldts": "6.1.13",
"zxcvbn": "4.4.2" "zxcvbn": "4.4.2"
}, },
"bin": { "bin": {
@@ -4409,9 +4409,9 @@
} }
}, },
"node_modules/@electron/asar": { "node_modules/@electron/asar": {
"version": "3.2.8", "version": "3.2.9",
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.8.tgz", "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.9.tgz",
"integrity": "sha512-cmskk5M06ewHMZAplSiF4AlME3IrnnZhKnWbtwKVLRkdJkKyUVjMLhDIiPIx/+6zQWVlKX/LtmK9xDme7540Sg==", "integrity": "sha512-Vu2P3X2gcZ3MY9W7yH72X9+AMXwUQZEJBrsPIbX0JsdllLtoh62/Q8Wg370/DawIEVKOyfD6KtTLo645ezqxUA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"commander": "^5.0.0", "commander": "^5.0.0",
@@ -4607,9 +4607,9 @@
} }
}, },
"node_modules/@electron/universal": { "node_modules/@electron/universal": {
"version": "1.4.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.4.1.tgz", "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.5.1.tgz",
"integrity": "sha512-lE/U3UNw1YHuowNbTmKNs9UlS3En3cPgwM5MI+agIgr/B1hSze9NdOP0qn7boZaI9Lph8IDv3/24g9IxnJP7aQ==", "integrity": "sha512-kbgXxyEauPJiQQUNG2VgUeyfQNFk6hBF11ISN2PNI6agUgPl55pv4eQmaqHzTAzchBvqZ2tQuRVaPStGf0mxGw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@electron/asar": "^3.2.1", "@electron/asar": "^3.2.1",
@@ -5137,9 +5137,9 @@
} }
}, },
"node_modules/@eslint/js": { "node_modules/@eslint/js": {
"version": "8.56.0", "version": "8.57.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz",
"integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0" "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -5306,13 +5306,13 @@
} }
}, },
"node_modules/@humanwhocodes/config-array": { "node_modules/@humanwhocodes/config-array": {
"version": "0.11.13", "version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
"integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@humanwhocodes/object-schema": "^2.0.1", "@humanwhocodes/object-schema": "^2.0.2",
"debug": "^4.1.1", "debug": "^4.3.1",
"minimatch": "^3.0.5" "minimatch": "^3.0.5"
}, },
"engines": { "engines": {
@@ -5333,9 +5333,9 @@
} }
}, },
"node_modules/@humanwhocodes/object-schema": { "node_modules/@humanwhocodes/object-schema": {
"version": "2.0.1", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz",
"integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==",
"dev": true "dev": true
}, },
"node_modules/@isaacs/cliui": { "node_modules/@isaacs/cliui": {
@@ -11039,9 +11039,9 @@
"dev": true "dev": true
}, },
"node_modules/@types/firefox-webext-browser": { "node_modules/@types/firefox-webext-browser": {
"version": "111.0.1", "version": "111.0.5",
"resolved": "https://registry.npmjs.org/@types/firefox-webext-browser/-/firefox-webext-browser-111.0.1.tgz", "resolved": "https://registry.npmjs.org/@types/firefox-webext-browser/-/firefox-webext-browser-111.0.5.tgz",
"integrity": "sha512-mmHWdQTCT68X0hh0URrsIyWhJeFzZHaiprj6nni/CmsAmqYq27T0eZyu1ePeKJ/zuDD3wqtTzm5TwRFAso+oPw==", "integrity": "sha512-YYE+4MeJvq7DZ+UzPD8c5uN1HJpGu4Fl6O6PEAfBJQmLzQkfTWlgMjZMJQHAmcH3rjVS5fjN+jMkkZ4ZTlKbmA==",
"dev": true "dev": true
}, },
"node_modules/@types/fs-extra": { "node_modules/@types/fs-extra": {
@@ -13302,26 +13302,25 @@
"dev": true "dev": true
}, },
"node_modules/app-builder-lib": { "node_modules/app-builder-lib": {
"version": "24.9.1", "version": "24.13.3",
"resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-24.9.1.tgz", "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-24.13.3.tgz",
"integrity": "sha512-Q1nYxZcio4r+W72cnIRVYofEAyjBd3mG47o+zms8HlD51zWtA/YxJb01Jei5F+jkWhge/PTQK+uldsPh6d0/4g==", "integrity": "sha512-FAzX6IBit2POXYGnTCT8YHFO/lr5AapAII6zzhQO3Rw4cEDOgK+t1xhLc5tNcKlicTHlo9zxIwnYCX9X2DLkig==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@develar/schema-utils": "~2.6.5", "@develar/schema-utils": "~2.6.5",
"@electron/notarize": "2.1.0", "@electron/notarize": "2.2.1",
"@electron/osx-sign": "1.0.5", "@electron/osx-sign": "1.0.5",
"@electron/universal": "1.4.1", "@electron/universal": "1.5.1",
"@malept/flatpak-bundler": "^0.4.0", "@malept/flatpak-bundler": "^0.4.0",
"@types/fs-extra": "9.0.13", "@types/fs-extra": "9.0.13",
"7zip-bin": "~5.2.0",
"async-exit-hook": "^2.0.1", "async-exit-hook": "^2.0.1",
"bluebird-lst": "^1.0.9", "bluebird-lst": "^1.0.9",
"builder-util": "24.8.1", "builder-util": "24.13.1",
"builder-util-runtime": "9.2.3", "builder-util-runtime": "9.2.4",
"chromium-pickle-js": "^0.2.0", "chromium-pickle-js": "^0.2.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"ejs": "^3.1.8", "ejs": "^3.1.8",
"electron-publish": "24.8.1", "electron-publish": "24.13.1",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"fs-extra": "^10.1.0", "fs-extra": "^10.1.0",
"hosted-git-info": "^4.1.0", "hosted-git-info": "^4.1.0",
@@ -13338,12 +13337,16 @@
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
},
"peerDependencies": {
"dmg-builder": "24.13.3",
"electron-builder-squirrel-windows": "24.13.3"
} }
}, },
"node_modules/app-builder-lib/node_modules/@electron/notarize": { "node_modules/app-builder-lib/node_modules/@electron/notarize": {
"version": "2.1.0", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.1.0.tgz", "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.2.1.tgz",
"integrity": "sha512-Q02xem1D0sg4v437xHgmBLxI2iz/fc0D4K7fiVWHa/AnW8o7D751xyKNXgziA6HrTOme9ul1JfWN5ark8WH1xA==", "integrity": "sha512-aL+bFMIkpR0cmmj5Zgy0LMKEpgy43/hw5zadEArgmAMWWlKc5buwFvFT9G/o/YJkvXAJm5q3iuTuLaiaXW39sg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"debug": "^4.1.1", "debug": "^4.1.1",
@@ -13384,6 +13387,19 @@
"balanced-match": "^1.0.0" "balanced-match": "^1.0.0"
} }
}, },
"node_modules/app-builder-lib/node_modules/builder-util-runtime": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz",
"integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
"sax": "^1.2.4"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/app-builder-lib/node_modules/js-yaml": { "node_modules/app-builder-lib/node_modules/js-yaml": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
@@ -13436,6 +13452,83 @@
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
"integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ=="
}, },
"node_modules/archiver": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz",
"integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==",
"dev": true,
"peer": true,
"dependencies": {
"archiver-utils": "^2.1.0",
"async": "^3.2.4",
"buffer-crc32": "^0.2.1",
"readable-stream": "^3.6.0",
"readdir-glob": "^1.1.2",
"tar-stream": "^2.2.0",
"zip-stream": "^4.1.0"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/archiver-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz",
"integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==",
"dev": true,
"peer": true,
"dependencies": {
"glob": "^7.1.4",
"graceful-fs": "^4.2.0",
"lazystream": "^1.0.0",
"lodash.defaults": "^4.2.0",
"lodash.difference": "^4.5.0",
"lodash.flatten": "^4.4.0",
"lodash.isplainobject": "^4.0.6",
"lodash.union": "^4.6.0",
"normalize-path": "^3.0.0",
"readable-stream": "^2.0.0"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/archiver-utils/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dev": true,
"peer": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/archiver/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dev": true,
"peer": true,
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/archy": { "node_modules/archy": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz",
@@ -15015,16 +15108,16 @@
} }
}, },
"node_modules/builder-util": { "node_modules/builder-util": {
"version": "24.8.1", "version": "24.13.1",
"resolved": "https://registry.npmjs.org/builder-util/-/builder-util-24.8.1.tgz", "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-24.13.1.tgz",
"integrity": "sha512-ibmQ4BnnqCnJTNrdmdNlnhF48kfqhNzSeqFMXHLIl+o9/yhn6QfOaVrloZ9YUu3m0k3rexvlT5wcki6LWpjTZw==", "integrity": "sha512-NhbCSIntruNDTOVI9fdXz0dihaqX2YuE1D6zZMrwiErzH4ELZHE6mdiB40wEgZNprDia+FghRFgKoAqMZRRjSA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/debug": "^4.1.6", "@types/debug": "^4.1.6",
"7zip-bin": "~5.2.0", "7zip-bin": "~5.2.0",
"app-builder-bin": "4.0.0", "app-builder-bin": "4.0.0",
"bluebird-lst": "^1.0.9", "bluebird-lst": "^1.0.9",
"builder-util-runtime": "9.2.3", "builder-util-runtime": "9.2.4",
"chalk": "^4.1.2", "chalk": "^4.1.2",
"cross-spawn": "^7.0.3", "cross-spawn": "^7.0.3",
"debug": "^4.3.4", "debug": "^4.3.4",
@@ -15057,6 +15150,19 @@
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true "dev": true
}, },
"node_modules/builder-util/node_modules/builder-util-runtime": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz",
"integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
"sax": "^1.2.4"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/builder-util/node_modules/https-proxy-agent": { "node_modules/builder-util/node_modules/https-proxy-agent": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
@@ -16125,6 +16231,37 @@
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==",
"dev": true "dev": true
}, },
"node_modules/compress-commons": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz",
"integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==",
"dev": true,
"peer": true,
"dependencies": {
"buffer-crc32": "^0.2.13",
"crc32-stream": "^4.0.2",
"normalize-path": "^3.0.0",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/compress-commons/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dev": true,
"peer": true,
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/compressible": { "node_modules/compressible": {
"version": "2.0.18", "version": "2.0.18",
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
@@ -16658,6 +16795,19 @@
"buffer": "^5.1.0" "buffer": "^5.1.0"
} }
}, },
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"dev": true,
"peer": true,
"bin": {
"crc32": "bin/crc32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/crc/node_modules/buffer": { "node_modules/crc/node_modules/buffer": {
"version": "5.7.1", "version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
@@ -16683,6 +16833,35 @@
"ieee754": "^1.1.13" "ieee754": "^1.1.13"
} }
}, },
"node_modules/crc32-stream": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz",
"integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==",
"dev": true,
"peer": true,
"dependencies": {
"crc-32": "^1.2.0",
"readable-stream": "^3.4.0"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/crc32-stream/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dev": true,
"peer": true,
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/credit-card-type": { "node_modules/credit-card-type": {
"version": "9.1.0", "version": "9.1.0",
"resolved": "https://registry.npmjs.org/credit-card-type/-/credit-card-type-9.1.0.tgz", "resolved": "https://registry.npmjs.org/credit-card-type/-/credit-card-type-9.1.0.tgz",
@@ -17406,14 +17585,14 @@
"dev": true "dev": true
}, },
"node_modules/dmg-builder": { "node_modules/dmg-builder": {
"version": "24.9.1", "version": "24.13.3",
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.9.1.tgz", "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.13.3.tgz",
"integrity": "sha512-huC+O6hvHd24Ubj3cy2GMiGLe2xGFKN3klqVMLAdcbB6SWMd1yPSdZvV8W1O01ICzCCRlZDHiv4VrNUgnPUfbQ==", "integrity": "sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"app-builder-lib": "24.9.1", "app-builder-lib": "24.13.3",
"builder-util": "24.8.1", "builder-util": "24.13.1",
"builder-util-runtime": "9.2.3", "builder-util-runtime": "9.2.4",
"fs-extra": "^10.1.0", "fs-extra": "^10.1.0",
"iconv-lite": "^0.6.2", "iconv-lite": "^0.6.2",
"js-yaml": "^4.1.0" "js-yaml": "^4.1.0"
@@ -17428,6 +17607,19 @@
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true "dev": true
}, },
"node_modules/dmg-builder/node_modules/builder-util-runtime": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz",
"integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
"sax": "^1.2.4"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/dmg-builder/node_modules/js-yaml": { "node_modules/dmg-builder/node_modules/js-yaml": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
@@ -17825,16 +18017,16 @@
} }
}, },
"node_modules/electron-builder": { "node_modules/electron-builder": {
"version": "24.9.1", "version": "24.13.3",
"resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.9.1.tgz", "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.13.3.tgz",
"integrity": "sha512-v7BuakDuY6sKMUYM8mfQGrwyjBpZ/ObaqnenU0H+igEL10nc6ht049rsCw2HghRBdEwJxGIBuzs3jbEhNaMDmg==", "integrity": "sha512-yZSgVHft5dNVlo31qmJAe4BVKQfFdwpRw7sFp1iQglDRCDD6r22zfRJuZlhtB5gp9FHUxCMEoWGq10SkCnMAIg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"app-builder-lib": "24.9.1", "app-builder-lib": "24.13.3",
"builder-util": "24.8.1", "builder-util": "24.13.1",
"builder-util-runtime": "9.2.3", "builder-util-runtime": "9.2.4",
"chalk": "^4.1.2", "chalk": "^4.1.2",
"dmg-builder": "24.9.1", "dmg-builder": "24.13.3",
"fs-extra": "^10.1.0", "fs-extra": "^10.1.0",
"is-ci": "^3.0.0", "is-ci": "^3.0.0",
"lazy-val": "^1.0.5", "lazy-val": "^1.0.5",
@@ -17850,6 +18042,32 @@
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/electron-builder-squirrel-windows": {
"version": "24.13.3",
"resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-24.13.3.tgz",
"integrity": "sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==",
"dev": true,
"peer": true,
"dependencies": {
"app-builder-lib": "24.13.3",
"archiver": "^5.3.1",
"builder-util": "24.13.1",
"fs-extra": "^10.1.0"
}
},
"node_modules/electron-builder/node_modules/builder-util-runtime": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz",
"integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
"sax": "^1.2.4"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/electron-log": { "node_modules/electron-log": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/electron-log/-/electron-log-5.0.1.tgz", "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-5.0.1.tgz",
@@ -17860,20 +18078,33 @@
} }
}, },
"node_modules/electron-publish": { "node_modules/electron-publish": {
"version": "24.8.1", "version": "24.13.1",
"resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.8.1.tgz", "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.13.1.tgz",
"integrity": "sha512-IFNXkdxMVzUdweoLJNXSupXkqnvgbrn3J4vognuOY06LaS/m0xvfFYIf+o1CM8if6DuWYWoQFKPcWZt/FUjZPw==", "integrity": "sha512-2ZgdEqJ8e9D17Hwp5LEq5mLQPjqU3lv/IALvgp+4W8VeNhryfGhYEQC/PgDPMrnWUp+l60Ou5SJLsu+k4mhQ8A==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/fs-extra": "^9.0.11", "@types/fs-extra": "^9.0.11",
"builder-util": "24.8.1", "builder-util": "24.13.1",
"builder-util-runtime": "9.2.3", "builder-util-runtime": "9.2.4",
"chalk": "^4.1.2", "chalk": "^4.1.2",
"fs-extra": "^10.1.0", "fs-extra": "^10.1.0",
"lazy-val": "^1.0.5", "lazy-val": "^1.0.5",
"mime": "^2.5.2" "mime": "^2.5.2"
} }
}, },
"node_modules/electron-publish/node_modules/builder-util-runtime": {
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz",
"integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
"sax": "^1.2.4"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/electron-reload": { "node_modules/electron-reload": {
"version": "2.0.0-alpha.1", "version": "2.0.0-alpha.1",
"resolved": "https://registry.npmjs.org/electron-reload/-/electron-reload-2.0.0-alpha.1.tgz", "resolved": "https://registry.npmjs.org/electron-reload/-/electron-reload-2.0.0-alpha.1.tgz",
@@ -18445,16 +18676,16 @@
} }
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "8.56.0", "version": "8.57.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
"integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1", "@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^2.1.4", "@eslint/eslintrc": "^2.1.4",
"@eslint/js": "8.56.0", "@eslint/js": "8.57.0",
"@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/config-array": "^0.11.14",
"@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8", "@nodelib/fs.walk": "^1.2.8",
"@ungap/structured-clone": "^1.2.0", "@ungap/structured-clone": "^1.2.0",
@@ -18741,9 +18972,9 @@
} }
}, },
"node_modules/eslint-plugin-tailwindcss": { "node_modules/eslint-plugin-tailwindcss": {
"version": "3.13.1", "version": "3.14.3",
"resolved": "https://registry.npmjs.org/eslint-plugin-tailwindcss/-/eslint-plugin-tailwindcss-3.13.1.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-tailwindcss/-/eslint-plugin-tailwindcss-3.14.3.tgz",
"integrity": "sha512-2Nlgr9doO6vFAG9w4iGU0sspWXuzypfng10HTF+dFS2NterhweWtgdRvf/f7aaoOUUxVZM8wMIXzazrZ7CxyeA==", "integrity": "sha512-1MKT8CrVuqVJleHxb7ICHsF2QwO0G+VJ28athTtlcOkccp0qmwK7nCUa1C9paCZ+VVgQU4fonsjLz/wUxoMHJQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"fast-glob": "^3.2.5", "fast-glob": "^3.2.5",
@@ -18753,7 +18984,7 @@
"node": ">=12.13.0" "node": ">=12.13.0"
}, },
"peerDependencies": { "peerDependencies": {
"tailwindcss": "^3.3.2" "tailwindcss": "^3.4.0"
} }
}, },
"node_modules/eslint-scope": { "node_modules/eslint-scope": {
@@ -26242,18 +26473,46 @@
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
"dev": true "dev": true
}, },
"node_modules/lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
"dev": true,
"peer": true
},
"node_modules/lodash.difference": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
"integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==",
"dev": true,
"peer": true
},
"node_modules/lodash.escaperegexp": { "node_modules/lodash.escaperegexp": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
"integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==",
"dev": true "dev": true
}, },
"node_modules/lodash.flatten": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
"integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==",
"dev": true,
"peer": true
},
"node_modules/lodash.isequal": { "node_modules/lodash.isequal": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
"dev": true "dev": true
}, },
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"dev": true,
"peer": true
},
"node_modules/lodash.memoize": { "node_modules/lodash.memoize": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@@ -26266,6 +26525,13 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true "dev": true
}, },
"node_modules/lodash.union": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
"integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==",
"dev": true,
"peer": true
},
"node_modules/log-symbols": { "node_modules/log-symbols": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
@@ -32209,6 +32475,39 @@
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
}, },
"node_modules/readdir-glob": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz",
"integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
"dev": true,
"peer": true,
"dependencies": {
"minimatch": "^5.1.0"
}
},
"node_modules/readdir-glob/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"peer": true,
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/readdir-glob/node_modules/minimatch": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"dev": true,
"peer": true,
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/readdirp": { "node_modules/readdirp": {
"version": "3.6.0", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -35115,9 +35414,9 @@
} }
}, },
"node_modules/style-loader": { "node_modules/style-loader": {
"version": "3.3.3", "version": "3.3.4",
"resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz",
"integrity": "sha512-53BiGLXAcll9maCYtZi2RCQZKa8NQQai5C4horqKyRmHj9H7QmcUyucrH+4KW/gBQbXM2AsB0axoEcFZPlfPcw==", "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">= 12.13.0" "node": ">= 12.13.0"
@@ -35874,20 +36173,20 @@
"dev": true "dev": true
}, },
"node_modules/tldts": { "node_modules/tldts": {
"version": "6.1.11", "version": "6.1.13",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.11.tgz", "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.13.tgz",
"integrity": "sha512-AAgE/IWvbsg4Lr4KGFNR7bL/MhQfBlgGV9UBg2uy5mCwSGi5f12eZ7ZydAqv4ACys6pUYjNoV2qfZdcCn4RS+Q==", "integrity": "sha512-+GxHFKVHvUTg2ieNPTx3b/NpZbgJSTZEDdI4cJzTjVYDuxijeHi1tt7CHHsMjLqyc+T50VVgWs3LIb2LrXOzxw==",
"dependencies": { "dependencies": {
"tldts-core": "^6.1.11" "tldts-core": "^6.1.13"
}, },
"bin": { "bin": {
"tldts": "bin/cli.js" "tldts": "bin/cli.js"
} }
}, },
"node_modules/tldts-core": { "node_modules/tldts-core": {
"version": "6.1.11", "version": "6.1.13",
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.11.tgz", "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.13.tgz",
"integrity": "sha512-ZFcT+/fdEc5VRndQIJtArNBHsaq4udRoeE4E6cwLzGaH0dq7Ng2L7cAoea6riM2uhNFD09EDa1bN8lrfrOBCLg==" "integrity": "sha512-M1XP4D13YtXARKroULnLsKKuI1NCRAbJmUGGoXqWinajIDOhTeJf/trYUyBoLVx1/Nx1KBKxCrlW57ZW9cMHAA=="
}, },
"node_modules/tmp": { "node_modules/tmp": {
"version": "0.0.33", "version": "0.0.33",
@@ -38513,6 +38812,79 @@
"integrity": "sha512-C1x6lfvBICFTQIMgbt3JqMOno3VOtkWat/xEakLTOurskYIHPmzJrzd1e8BnmtdDVJlGuk5D+FxyCA8MPmkIyA==", "integrity": "sha512-C1x6lfvBICFTQIMgbt3JqMOno3VOtkWat/xEakLTOurskYIHPmzJrzd1e8BnmtdDVJlGuk5D+FxyCA8MPmkIyA==",
"dev": true "dev": true
}, },
"node_modules/zip-stream": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz",
"integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==",
"dev": true,
"peer": true,
"dependencies": {
"archiver-utils": "^3.0.4",
"compress-commons": "^4.1.2",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/zip-stream/node_modules/archiver-utils": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz",
"integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==",
"dev": true,
"peer": true,
"dependencies": {
"glob": "^7.2.3",
"graceful-fs": "^4.2.0",
"lazystream": "^1.0.0",
"lodash.defaults": "^4.2.0",
"lodash.difference": "^4.5.0",
"lodash.flatten": "^4.4.0",
"lodash.isplainobject": "^4.0.6",
"lodash.union": "^4.6.0",
"normalize-path": "^3.0.0",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/zip-stream/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dev": true,
"peer": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/zip-stream/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dev": true,
"peer": true,
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/zone.js": { "node_modules/zone.js": {
"version": "0.13.3", "version": "0.13.3",
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.13.3.tgz", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.13.3.tgz",

View File

@@ -57,7 +57,7 @@
"@types/argon2-browser": "1.18.1", "@types/argon2-browser": "1.18.1",
"@types/chrome": "0.0.262", "@types/chrome": "0.0.262",
"@types/duo_web_sdk": "2.7.1", "@types/duo_web_sdk": "2.7.1",
"@types/firefox-webext-browser": "111.0.1", "@types/firefox-webext-browser": "111.0.5",
"@types/inquirer": "8.2.10", "@types/inquirer": "8.2.10",
"@types/jest": "29.5.12", "@types/jest": "29.5.12",
"@types/jquery": "3.5.29", "@types/jquery": "3.5.29",
@@ -90,19 +90,19 @@
"cross-env": "7.0.3", "cross-env": "7.0.3",
"css-loader": "6.8.1", "css-loader": "6.8.1",
"electron": "28.2.6", "electron": "28.2.6",
"electron-builder": "24.9.1", "electron-builder": "24.13.3",
"electron-log": "5.0.1", "electron-log": "5.0.1",
"electron-reload": "2.0.0-alpha.1", "electron-reload": "2.0.0-alpha.1",
"electron-store": "8.1.0", "electron-store": "8.1.0",
"electron-updater": "6.1.8", "electron-updater": "6.1.8",
"eslint": "8.56.0", "eslint": "8.57.0",
"eslint-config-prettier": "9.1.0", "eslint-config-prettier": "9.1.0",
"eslint-import-resolver-typescript": "3.6.1", "eslint-import-resolver-typescript": "3.6.1",
"eslint-plugin-import": "2.29.1", "eslint-plugin-import": "2.29.1",
"eslint-plugin-rxjs": "5.0.3", "eslint-plugin-rxjs": "5.0.3",
"eslint-plugin-rxjs-angular": "2.0.1", "eslint-plugin-rxjs-angular": "2.0.1",
"eslint-plugin-storybook": "0.8.0", "eslint-plugin-storybook": "0.8.0",
"eslint-plugin-tailwindcss": "3.13.1", "eslint-plugin-tailwindcss": "3.14.3",
"gulp": "4.0.2", "gulp": "4.0.2",
"gulp-filter": "9.0.1", "gulp-filter": "9.0.1",
"gulp-if": "3.0.0", "gulp-if": "3.0.0",
@@ -133,7 +133,7 @@
"sass": "1.69.5", "sass": "1.69.5",
"sass-loader": "13.3.3", "sass-loader": "13.3.3",
"storybook": "7.6.17", "storybook": "7.6.17",
"style-loader": "3.3.3", "style-loader": "3.3.4",
"tailwindcss": "3.4.1", "tailwindcss": "3.4.1",
"ts-jest": "29.1.2", "ts-jest": "29.1.2",
"ts-loader": "9.5.1", "ts-loader": "9.5.1",
@@ -201,7 +201,7 @@
"qrious": "4.0.2", "qrious": "4.0.2",
"rxjs": "7.8.1", "rxjs": "7.8.1",
"tabbable": "6.2.0", "tabbable": "6.2.0",
"tldts": "6.1.11", "tldts": "6.1.13",
"utf-8-validate": "6.0.3", "utf-8-validate": "6.0.3",
"zone.js": "0.13.3", "zone.js": "0.13.3",
"zxcvbn": "4.4.2" "zxcvbn": "4.4.2"