mirror of
https://github.com/bitwarden/browser
synced 2026-02-13 06:54:07 +00:00
Merge branch 'main' into ps/extension-refresh
This commit is contained in:
@@ -604,6 +604,15 @@
|
||||
"yourVaultIsLocked": {
|
||||
"message": "Your vault is locked. Verify your identity to continue."
|
||||
},
|
||||
"yourVaultIsLockedV2": {
|
||||
"message": "Your vault is locked"
|
||||
},
|
||||
"yourAccountIsLocked": {
|
||||
"message": "Your account is locked"
|
||||
},
|
||||
"or": {
|
||||
"message": "or"
|
||||
},
|
||||
"unlock": {
|
||||
"message": "Unlock"
|
||||
},
|
||||
@@ -1936,6 +1945,9 @@
|
||||
"unlockWithBiometrics": {
|
||||
"message": "Unlock with biometrics"
|
||||
},
|
||||
"unlockWithMasterPassword": {
|
||||
"message": "Unlock with master password"
|
||||
},
|
||||
"awaitDesktop": {
|
||||
"message": "Awaiting confirmation from desktop"
|
||||
},
|
||||
@@ -3623,6 +3635,9 @@
|
||||
"typePasskey": {
|
||||
"message": "Passkey"
|
||||
},
|
||||
"accessing": {
|
||||
"message": "Accessing"
|
||||
},
|
||||
"passkeyNotCopied": {
|
||||
"message": "Passkey will not be copied"
|
||||
},
|
||||
|
||||
@@ -59,7 +59,7 @@ export class CurrentAccountComponent {
|
||||
}
|
||||
|
||||
async currentAccountClicked() {
|
||||
if (this.route.snapshot.data.state.includes("account-switcher")) {
|
||||
if (this.route.snapshot.data?.state?.includes("account-switcher")) {
|
||||
this.location.back();
|
||||
} else {
|
||||
await this.router.navigate(["/account-switcher"]);
|
||||
|
||||
@@ -17,6 +17,8 @@ import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh
|
||||
import {
|
||||
AnonLayoutWrapperComponent,
|
||||
AnonLayoutWrapperData,
|
||||
LockIcon,
|
||||
LockV2Component,
|
||||
PasswordHintComponent,
|
||||
RegistrationFinishComponent,
|
||||
RegistrationStartComponent,
|
||||
@@ -181,6 +183,7 @@ const routes: Routes = [
|
||||
path: "lock",
|
||||
component: LockComponent,
|
||||
canActivate: [lockGuard()],
|
||||
canMatch: [extensionRefreshRedirect("/lockV2")],
|
||||
data: { state: "lock", doNotSaveUrl: true } satisfies RouteDataProperties,
|
||||
},
|
||||
...twofactorRefactorSwap(
|
||||
@@ -438,6 +441,28 @@ const routes: Routes = [
|
||||
],
|
||||
},
|
||||
),
|
||||
{
|
||||
path: "",
|
||||
component: ExtensionAnonLayoutWrapperComponent,
|
||||
children: [
|
||||
{
|
||||
path: "lockV2",
|
||||
canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh), lockGuard()],
|
||||
data: {
|
||||
pageIcon: LockIcon,
|
||||
pageTitle: "yourVaultIsLockedV2",
|
||||
showReadonlyHostname: true,
|
||||
showAcctSwitcher: true,
|
||||
} satisfies ExtensionAnonLayoutWrapperData,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: LockV2Component,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: AnonLayoutWrapperComponent,
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
CLIENT_TYPE,
|
||||
} from "@bitwarden/angular/services/injection-tokens";
|
||||
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
||||
import { AnonLayoutWrapperDataService } from "@bitwarden/auth/angular";
|
||||
import { AnonLayoutWrapperDataService, LockComponentService } from "@bitwarden/auth/angular";
|
||||
import { LockService, PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
||||
@@ -117,6 +117,7 @@ import { ForegroundTaskSchedulerService } from "../../platform/services/task-sch
|
||||
import { BrowserStorageServiceProvider } from "../../platform/storage/browser-storage-service.provider";
|
||||
import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service";
|
||||
import { fromChromeRuntimeMessaging } from "../../platform/utils/from-chrome-runtime-messaging";
|
||||
import { ExtensionLockComponentService } from "../../services/extension-lock-component.service";
|
||||
import { ForegroundVaultTimeoutService } from "../../services/vault-timeout/foreground-vault-timeout.service";
|
||||
import { BrowserSendStateService } from "../../tools/popup/services/browser-send-state.service";
|
||||
import { FilePopoutUtilsService } from "../../tools/popup/services/file-popout-utils.service";
|
||||
@@ -536,6 +537,11 @@ const safeProviders: SafeProvider[] = [
|
||||
provide: CLIENT_TYPE,
|
||||
useValue: ClientType.Browser,
|
||||
}),
|
||||
safeProvider({
|
||||
provide: LockComponentService,
|
||||
useClass: ExtensionLockComponentService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: Fido2UserVerificationService,
|
||||
useClass: Fido2UserVerificationService,
|
||||
|
||||
@@ -0,0 +1,325 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { BiometricsDisableReason, UnlockOptions } from "@bitwarden/auth/angular";
|
||||
import {
|
||||
PinServiceAbstraction,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { BiometricsService } from "@bitwarden/key-management";
|
||||
|
||||
import { BrowserRouterService } from "../platform/popup/services/browser-router.service";
|
||||
|
||||
import { ExtensionLockComponentService } from "./extension-lock-component.service";
|
||||
|
||||
describe("ExtensionLockComponentService", () => {
|
||||
let service: ExtensionLockComponentService;
|
||||
|
||||
let userDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
|
||||
let platformUtilsService: MockProxy<PlatformUtilsService>;
|
||||
let biometricsService: MockProxy<BiometricsService>;
|
||||
let pinService: MockProxy<PinServiceAbstraction>;
|
||||
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
|
||||
let cryptoService: MockProxy<CryptoService>;
|
||||
let routerService: MockProxy<BrowserRouterService>;
|
||||
|
||||
beforeEach(() => {
|
||||
userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
|
||||
platformUtilsService = mock<PlatformUtilsService>();
|
||||
biometricsService = mock<BiometricsService>();
|
||||
pinService = mock<PinServiceAbstraction>();
|
||||
vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
|
||||
cryptoService = mock<CryptoService>();
|
||||
routerService = mock<BrowserRouterService>();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
ExtensionLockComponentService,
|
||||
{
|
||||
provide: UserDecryptionOptionsServiceAbstraction,
|
||||
useValue: userDecryptionOptionsService,
|
||||
},
|
||||
{
|
||||
provide: PlatformUtilsService,
|
||||
useValue: platformUtilsService,
|
||||
},
|
||||
{
|
||||
provide: BiometricsService,
|
||||
useValue: biometricsService,
|
||||
},
|
||||
{
|
||||
provide: PinServiceAbstraction,
|
||||
useValue: pinService,
|
||||
},
|
||||
{
|
||||
provide: VaultTimeoutSettingsService,
|
||||
useValue: vaultTimeoutSettingsService,
|
||||
},
|
||||
{
|
||||
provide: CryptoService,
|
||||
useValue: cryptoService,
|
||||
},
|
||||
{
|
||||
provide: BrowserRouterService,
|
||||
useValue: routerService,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.inject(ExtensionLockComponentService);
|
||||
});
|
||||
|
||||
it("instantiates", () => {
|
||||
expect(service).not.toBeFalsy();
|
||||
});
|
||||
|
||||
describe("getPreviousUrl", () => {
|
||||
it("returns the previous URL", () => {
|
||||
routerService.getPreviousUrl.mockReturnValue("previousUrl");
|
||||
expect(service.getPreviousUrl()).toBe("previousUrl");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBiometricsError", () => {
|
||||
it("returns a biometric error description when given a valid error type", () => {
|
||||
expect(
|
||||
service.getBiometricsError({
|
||||
message: "startDesktop",
|
||||
}),
|
||||
).toBe("startDesktopDesc");
|
||||
});
|
||||
|
||||
it("returns null when given an invalid error type", () => {
|
||||
expect(
|
||||
service.getBiometricsError({
|
||||
message: "invalidError",
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when given a null input", () => {
|
||||
expect(service.getBiometricsError(null)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("isWindowVisible", () => {
|
||||
it("throws an error", async () => {
|
||||
await expect(service.isWindowVisible()).rejects.toThrow("Method not implemented.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBiometricsUnlockBtnText", () => {
|
||||
it("returns the biometric unlock button text", () => {
|
||||
expect(service.getBiometricsUnlockBtnText()).toBe("unlockWithBiometrics");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAvailableUnlockOptions$", () => {
|
||||
interface MockInputs {
|
||||
hasMasterPassword: boolean;
|
||||
osSupportsBiometric: boolean;
|
||||
biometricLockSet: boolean;
|
||||
hasBiometricEncryptedUserKeyStored: boolean;
|
||||
platformSupportsSecureStorage: boolean;
|
||||
pinDecryptionAvailable: boolean;
|
||||
}
|
||||
|
||||
const table: [MockInputs, UnlockOptions][] = [
|
||||
[
|
||||
// MP + PIN + Biometrics available
|
||||
{
|
||||
hasMasterPassword: true,
|
||||
osSupportsBiometric: true,
|
||||
biometricLockSet: true,
|
||||
hasBiometricEncryptedUserKeyStored: true,
|
||||
platformSupportsSecureStorage: true,
|
||||
pinDecryptionAvailable: true,
|
||||
},
|
||||
{
|
||||
masterPassword: {
|
||||
enabled: true,
|
||||
},
|
||||
pin: {
|
||||
enabled: true,
|
||||
},
|
||||
biometrics: {
|
||||
enabled: true,
|
||||
disableReason: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
// PIN + Biometrics available
|
||||
{
|
||||
hasMasterPassword: false,
|
||||
osSupportsBiometric: true,
|
||||
biometricLockSet: true,
|
||||
hasBiometricEncryptedUserKeyStored: true,
|
||||
platformSupportsSecureStorage: true,
|
||||
pinDecryptionAvailable: true,
|
||||
},
|
||||
{
|
||||
masterPassword: {
|
||||
enabled: false,
|
||||
},
|
||||
pin: {
|
||||
enabled: true,
|
||||
},
|
||||
biometrics: {
|
||||
enabled: true,
|
||||
disableReason: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
// Biometrics available: user key stored with no secure storage
|
||||
{
|
||||
hasMasterPassword: false,
|
||||
osSupportsBiometric: true,
|
||||
biometricLockSet: true,
|
||||
hasBiometricEncryptedUserKeyStored: true,
|
||||
platformSupportsSecureStorage: false,
|
||||
pinDecryptionAvailable: false,
|
||||
},
|
||||
{
|
||||
masterPassword: {
|
||||
enabled: false,
|
||||
},
|
||||
pin: {
|
||||
enabled: false,
|
||||
},
|
||||
biometrics: {
|
||||
enabled: true,
|
||||
disableReason: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
// Biometrics available: no user key stored with no secure storage
|
||||
{
|
||||
hasMasterPassword: false,
|
||||
osSupportsBiometric: true,
|
||||
biometricLockSet: true,
|
||||
hasBiometricEncryptedUserKeyStored: false,
|
||||
platformSupportsSecureStorage: false,
|
||||
pinDecryptionAvailable: false,
|
||||
},
|
||||
{
|
||||
masterPassword: {
|
||||
enabled: false,
|
||||
},
|
||||
pin: {
|
||||
enabled: false,
|
||||
},
|
||||
biometrics: {
|
||||
enabled: true,
|
||||
disableReason: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
// Biometrics not available: biometric lock not set
|
||||
{
|
||||
hasMasterPassword: false,
|
||||
osSupportsBiometric: true,
|
||||
biometricLockSet: false,
|
||||
hasBiometricEncryptedUserKeyStored: true,
|
||||
platformSupportsSecureStorage: true,
|
||||
pinDecryptionAvailable: false,
|
||||
},
|
||||
{
|
||||
masterPassword: {
|
||||
enabled: false,
|
||||
},
|
||||
pin: {
|
||||
enabled: false,
|
||||
},
|
||||
biometrics: {
|
||||
enabled: false,
|
||||
disableReason: BiometricsDisableReason.EncryptedKeysUnavailable,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
// Biometrics not available: user key not stored
|
||||
{
|
||||
hasMasterPassword: false,
|
||||
osSupportsBiometric: true,
|
||||
biometricLockSet: true,
|
||||
hasBiometricEncryptedUserKeyStored: false,
|
||||
platformSupportsSecureStorage: true,
|
||||
pinDecryptionAvailable: false,
|
||||
},
|
||||
{
|
||||
masterPassword: {
|
||||
enabled: false,
|
||||
},
|
||||
pin: {
|
||||
enabled: false,
|
||||
},
|
||||
biometrics: {
|
||||
enabled: false,
|
||||
disableReason: BiometricsDisableReason.EncryptedKeysUnavailable,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
// Biometrics not available: OS doesn't support
|
||||
{
|
||||
hasMasterPassword: false,
|
||||
osSupportsBiometric: false,
|
||||
biometricLockSet: true,
|
||||
hasBiometricEncryptedUserKeyStored: true,
|
||||
platformSupportsSecureStorage: true,
|
||||
pinDecryptionAvailable: false,
|
||||
},
|
||||
{
|
||||
masterPassword: {
|
||||
enabled: false,
|
||||
},
|
||||
pin: {
|
||||
enabled: false,
|
||||
},
|
||||
biometrics: {
|
||||
enabled: false,
|
||||
disableReason: BiometricsDisableReason.NotSupportedOnOperatingSystem,
|
||||
},
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
test.each(table)("returns unlock options", async (mockInputs, expectedOutput) => {
|
||||
const userId = "userId" as UserId;
|
||||
const userDecryptionOptions = {
|
||||
hasMasterPassword: mockInputs.hasMasterPassword,
|
||||
};
|
||||
|
||||
// MP
|
||||
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
|
||||
of(userDecryptionOptions),
|
||||
);
|
||||
|
||||
// Biometrics
|
||||
biometricsService.supportsBiometric.mockResolvedValue(mockInputs.osSupportsBiometric);
|
||||
vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(mockInputs.biometricLockSet);
|
||||
cryptoService.hasUserKeyStored.mockResolvedValue(
|
||||
mockInputs.hasBiometricEncryptedUserKeyStored,
|
||||
);
|
||||
platformUtilsService.supportsSecureStorage.mockReturnValue(
|
||||
mockInputs.platformSupportsSecureStorage,
|
||||
);
|
||||
|
||||
// PIN
|
||||
pinService.isPinDecryptionAvailable.mockResolvedValue(mockInputs.pinDecryptionAvailable);
|
||||
|
||||
const unlockOptions = await firstValueFrom(service.getAvailableUnlockOptions$(userId));
|
||||
|
||||
expect(unlockOptions).toEqual(expectedOutput);
|
||||
});
|
||||
});
|
||||
});
|
||||
117
apps/browser/src/services/extension-lock-component.service.ts
Normal file
117
apps/browser/src/services/extension-lock-component.service.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { inject } from "@angular/core";
|
||||
import { combineLatest, defer, map, Observable } from "rxjs";
|
||||
|
||||
import {
|
||||
BiometricsDisableReason,
|
||||
LockComponentService,
|
||||
UnlockOptions,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import {
|
||||
PinServiceAbstraction,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { KeySuffixOptions } from "@bitwarden/common/platform/enums";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { BiometricsService } from "@bitwarden/key-management";
|
||||
|
||||
import { BiometricErrors, BiometricErrorTypes } from "../models/biometricErrors";
|
||||
import { BrowserRouterService } from "../platform/popup/services/browser-router.service";
|
||||
|
||||
export class ExtensionLockComponentService implements LockComponentService {
|
||||
private readonly userDecryptionOptionsService = inject(UserDecryptionOptionsServiceAbstraction);
|
||||
private readonly platformUtilsService = inject(PlatformUtilsService);
|
||||
private readonly biometricsService = inject(BiometricsService);
|
||||
private readonly pinService = inject(PinServiceAbstraction);
|
||||
private readonly vaultTimeoutSettingsService = inject(VaultTimeoutSettingsService);
|
||||
private readonly cryptoService = inject(CryptoService);
|
||||
private readonly routerService = inject(BrowserRouterService);
|
||||
|
||||
getPreviousUrl(): string | null {
|
||||
return this.routerService.getPreviousUrl();
|
||||
}
|
||||
|
||||
getBiometricsError(error: any): string | null {
|
||||
const biometricsError = BiometricErrors[error?.message as BiometricErrorTypes];
|
||||
|
||||
if (!biometricsError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return biometricsError.description;
|
||||
}
|
||||
|
||||
async isWindowVisible(): Promise<boolean> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
getBiometricsUnlockBtnText(): string {
|
||||
return "unlockWithBiometrics";
|
||||
}
|
||||
|
||||
private async isBiometricLockSet(userId: UserId): Promise<boolean> {
|
||||
const biometricLockSet = await this.vaultTimeoutSettingsService.isBiometricLockSet(userId);
|
||||
const hasBiometricEncryptedUserKeyStored = await this.cryptoService.hasUserKeyStored(
|
||||
KeySuffixOptions.Biometric,
|
||||
userId,
|
||||
);
|
||||
const platformSupportsSecureStorage = this.platformUtilsService.supportsSecureStorage();
|
||||
|
||||
return (
|
||||
biometricLockSet && (hasBiometricEncryptedUserKeyStored || !platformSupportsSecureStorage)
|
||||
);
|
||||
}
|
||||
|
||||
private getBiometricsDisabledReason(
|
||||
osSupportsBiometric: boolean,
|
||||
biometricLockSet: boolean,
|
||||
): BiometricsDisableReason | null {
|
||||
if (!osSupportsBiometric) {
|
||||
return BiometricsDisableReason.NotSupportedOnOperatingSystem;
|
||||
} else if (!biometricLockSet) {
|
||||
return BiometricsDisableReason.EncryptedKeysUnavailable;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
getAvailableUnlockOptions$(userId: UserId): Observable<UnlockOptions> {
|
||||
return combineLatest([
|
||||
// Note: defer is preferable b/c it delays the execution of the function until the observable is subscribed to
|
||||
defer(() => this.biometricsService.supportsBiometric()),
|
||||
defer(() => this.isBiometricLockSet(userId)),
|
||||
this.userDecryptionOptionsService.userDecryptionOptionsById$(userId),
|
||||
defer(() => this.pinService.isPinDecryptionAvailable(userId)),
|
||||
]).pipe(
|
||||
map(
|
||||
([
|
||||
supportsBiometric,
|
||||
isBiometricsLockSet,
|
||||
userDecryptionOptions,
|
||||
pinDecryptionAvailable,
|
||||
]) => {
|
||||
const disableReason = this.getBiometricsDisabledReason(
|
||||
supportsBiometric,
|
||||
isBiometricsLockSet,
|
||||
);
|
||||
|
||||
const unlockOpts: UnlockOptions = {
|
||||
masterPassword: {
|
||||
enabled: userDecryptionOptions.hasMasterPassword,
|
||||
},
|
||||
pin: {
|
||||
enabled: pinDecryptionAvailable,
|
||||
},
|
||||
biometrics: {
|
||||
enabled: supportsBiometric && isBiometricsLockSet,
|
||||
disableReason: disableReason,
|
||||
},
|
||||
};
|
||||
return unlockOpts;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,8 @@
|
||||
<tools-send-form
|
||||
formId="sendForm"
|
||||
[config]="config"
|
||||
(sendSaved)="onSendSaved()"
|
||||
(onSendCreated)="onSendCreated($event)"
|
||||
(onSendUpdated)="onSendUpdated($event)"
|
||||
[submitBtn]="submitBtn"
|
||||
>
|
||||
</tools-send-form>
|
||||
|
||||
@@ -2,12 +2,13 @@ import { CommonModule, Location } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormsModule } from "@angular/forms";
|
||||
import { ActivatedRoute, Params } from "@angular/router";
|
||||
import { ActivatedRoute, Params, Router } from "@angular/router";
|
||||
import { map, switchMap } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
import { SendId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
@@ -95,14 +96,25 @@ export class SendAddEditComponent {
|
||||
private sendApiService: SendApiService,
|
||||
private toastService: ToastService,
|
||||
private dialogService: DialogService,
|
||||
private router: Router,
|
||||
) {
|
||||
this.subscribeToParams();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the event when the send is saved.
|
||||
* Handles the event when the send is created.
|
||||
*/
|
||||
onSendSaved() {
|
||||
async onSendCreated(send: SendView) {
|
||||
await this.router.navigate(["/send-created"], {
|
||||
queryParams: { sendId: send.id },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the event when the send is updated.
|
||||
*/
|
||||
onSendUpdated(send: SendView) {
|
||||
this.location.back();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<main class="tw-top-0">
|
||||
<popup-page>
|
||||
<popup-header slot="header" [pageTitle]="'createdSend' | i18n" showBackButton>
|
||||
<popup-header
|
||||
slot="header"
|
||||
[pageTitle]="'createdSend' | i18n"
|
||||
showBackButton
|
||||
[backAction]="close.bind(this)"
|
||||
>
|
||||
<ng-container slot="end">
|
||||
<app-pop-out></app-pop-out>
|
||||
</ng-container>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CommonModule, Location } from "@angular/common";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { ActivatedRoute, RouterLink } from "@angular/router";
|
||||
import { ActivatedRoute, Router, RouterLink } from "@angular/router";
|
||||
import { RouterTestingModule } from "@angular/router/testing";
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
@@ -33,6 +33,7 @@ describe("SendCreatedComponent", () => {
|
||||
let location: MockProxy<Location>;
|
||||
let activatedRoute: MockProxy<ActivatedRoute>;
|
||||
let environmentService: MockProxy<EnvironmentService>;
|
||||
let router: MockProxy<Router>;
|
||||
|
||||
const sendId = "test-send-id";
|
||||
const deletionDate = new Date();
|
||||
@@ -52,6 +53,7 @@ describe("SendCreatedComponent", () => {
|
||||
location = mock<Location>();
|
||||
activatedRoute = mock<ActivatedRoute>();
|
||||
environmentService = mock<EnvironmentService>();
|
||||
router = mock<Router>();
|
||||
Object.defineProperty(environmentService, "environment$", {
|
||||
configurable: true,
|
||||
get: () => of(new SelfHostedEnvironment({ webVault: "https://example.com" })),
|
||||
@@ -89,6 +91,7 @@ describe("SendCreatedComponent", () => {
|
||||
{ provide: ConfigService, useValue: mock<ConfigService>() },
|
||||
{ provide: EnvironmentService, useValue: environmentService },
|
||||
{ provide: PopupRouterCacheService, useValue: mock<PopupRouterCacheService>() },
|
||||
{ provide: Router, useValue: router },
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
@@ -109,10 +112,10 @@ describe("SendCreatedComponent", () => {
|
||||
expect(component["daysAvailable"]).toBe(7);
|
||||
});
|
||||
|
||||
it("should navigate back on close", () => {
|
||||
it("should navigate back to send list on close", async () => {
|
||||
fixture.detectChanges();
|
||||
component.close();
|
||||
expect(location.back).toHaveBeenCalled();
|
||||
await component.close();
|
||||
expect(router.navigate).toHaveBeenCalledWith(["/tabs/send"]);
|
||||
});
|
||||
|
||||
describe("getDaysAvailable", () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CommonModule, Location } from "@angular/common";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { ActivatedRoute, RouterLink } from "@angular/router";
|
||||
import { ActivatedRoute, Router, RouterLink, RouterModule } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
@@ -30,6 +30,7 @@ import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page
|
||||
PopupHeaderComponent,
|
||||
PopupPageComponent,
|
||||
RouterLink,
|
||||
RouterModule,
|
||||
PopupFooterComponent,
|
||||
IconModule,
|
||||
],
|
||||
@@ -45,10 +46,11 @@ export class SendCreatedComponent {
|
||||
private sendService: SendService,
|
||||
private route: ActivatedRoute,
|
||||
private toastService: ToastService,
|
||||
private location: Location,
|
||||
private router: Router,
|
||||
private environmentService: EnvironmentService,
|
||||
) {
|
||||
const sendId = this.route.snapshot.queryParamMap.get("sendId");
|
||||
|
||||
this.sendService.sendViews$.pipe(takeUntilDestroyed()).subscribe((sendViews) => {
|
||||
this.send = sendViews.find((s) => s.id === sendId);
|
||||
if (this.send) {
|
||||
@@ -62,8 +64,8 @@ export class SendCreatedComponent {
|
||||
return Math.max(0, Math.ceil((send.deletionDate.getTime() - now) / (1000 * 60 * 60 * 24)));
|
||||
}
|
||||
|
||||
close() {
|
||||
this.location.back();
|
||||
async close() {
|
||||
await this.router.navigate(["/tabs/send"]);
|
||||
}
|
||||
|
||||
async copyLink() {
|
||||
|
||||
@@ -11,9 +11,12 @@ import {
|
||||
unauthGuardFn,
|
||||
} from "@bitwarden/angular/auth/guards";
|
||||
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
|
||||
import { extensionRefreshRedirect } from "@bitwarden/angular/utils/extension-refresh-redirect";
|
||||
import {
|
||||
AnonLayoutWrapperComponent,
|
||||
AnonLayoutWrapperData,
|
||||
LockIcon,
|
||||
LockV2Component,
|
||||
PasswordHintComponent,
|
||||
RegistrationFinishComponent,
|
||||
RegistrationStartComponent,
|
||||
@@ -62,6 +65,7 @@ const routes: Routes = [
|
||||
path: "lock",
|
||||
component: LockComponent,
|
||||
canActivate: [lockGuard()],
|
||||
canMatch: [extensionRefreshRedirect("/lockV2")],
|
||||
},
|
||||
{
|
||||
path: "login",
|
||||
@@ -190,6 +194,21 @@ const routes: Routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "lockV2",
|
||||
canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh), lockGuard()],
|
||||
data: {
|
||||
pageIcon: LockIcon,
|
||||
pageTitle: "yourVaultIsLockedV2",
|
||||
showReadonlyHostname: true,
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: LockV2Component,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "set-password-jit",
|
||||
canActivate: [canAccessFeature(FeatureFlag.EmailVerification)],
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
CLIENT_TYPE,
|
||||
} from "@bitwarden/angular/services/injection-tokens";
|
||||
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
||||
import { SetPasswordJitService } from "@bitwarden/auth/angular";
|
||||
import { LockComponentService, SetPasswordJitService } from "@bitwarden/auth/angular";
|
||||
import {
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
PinServiceAbstraction,
|
||||
@@ -86,6 +86,7 @@ import { ElectronRendererStorageService } from "../../platform/services/electron
|
||||
import { I18nRendererService } from "../../platform/services/i18n.renderer.service";
|
||||
import { fromIpcMessaging } from "../../platform/utils/from-ipc-messaging";
|
||||
import { fromIpcSystemTheme } from "../../platform/utils/from-ipc-system-theme";
|
||||
import { DesktopLockComponentService } from "../../services/desktop-lock-component.service";
|
||||
import { EncryptedMessageHandlerService } from "../../services/encrypted-message-handler.service";
|
||||
import { NativeMessageHandlerService } from "../../services/native-message-handler.service";
|
||||
import { NativeMessagingService } from "../../services/native-messaging.service";
|
||||
@@ -277,6 +278,11 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: NativeMessagingManifestService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: LockComponentService,
|
||||
useClass: DesktopLockComponentService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: CLIENT_TYPE,
|
||||
useValue: ClientType.Desktop,
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
attr.aria-hidden="{{ showingModal }}"
|
||||
>
|
||||
<div id="content" class="content" style="padding-top: 50px">
|
||||
<img class="logo-image" alt="Bitwarden" />
|
||||
<a (click)="invalidateEmail()" class="tw-cursor-pointer">
|
||||
<img class="logo-image" alt="Bitwarden" />
|
||||
</a>
|
||||
<p class="lead">{{ "loginOrCreateNewAccount" | i18n }}</p>
|
||||
<!-- start email -->
|
||||
<ng-container *ngIf="!validatedEmail; else loginPage">
|
||||
|
||||
@@ -227,4 +227,11 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force the validatedEmail flag to false, which will show the login page.
|
||||
*/
|
||||
invalidateEmail() {
|
||||
this.validatedEmail = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -918,6 +918,18 @@
|
||||
"yourVaultIsLocked": {
|
||||
"message": "Your vault is locked. Verify your identity to continue."
|
||||
},
|
||||
"yourAccountIsLocked": {
|
||||
"message": "Your account is locked"
|
||||
},
|
||||
"or": {
|
||||
"message": "or"
|
||||
},
|
||||
"unlockWithBiometrics": {
|
||||
"message": "Unlock with biometrics"
|
||||
},
|
||||
"unlockWithMasterPassword": {
|
||||
"message": "Unlock with master password"
|
||||
},
|
||||
"unlock": {
|
||||
"message": "Unlock"
|
||||
},
|
||||
@@ -2256,6 +2268,9 @@
|
||||
"locked": {
|
||||
"message": "Locked"
|
||||
},
|
||||
"yourVaultIsLockedV2": {
|
||||
"message": "Your vault is locked"
|
||||
},
|
||||
"unlocked": {
|
||||
"message": "Unlocked"
|
||||
},
|
||||
@@ -2608,6 +2623,9 @@
|
||||
"important": {
|
||||
"message": "Important:"
|
||||
},
|
||||
"accessing": {
|
||||
"message": "Accessing"
|
||||
},
|
||||
"accessTokenUnableToBeDecrypted": {
|
||||
"message": "You have been logged out because your access token could not be decrypted. Please log in again to resolve this issue."
|
||||
},
|
||||
|
||||
377
apps/desktop/src/services/desktop-lock-component.service.spec.ts
Normal file
377
apps/desktop/src/services/desktop-lock-component.service.spec.ts
Normal file
@@ -0,0 +1,377 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { BiometricsDisableReason, UnlockOptions } from "@bitwarden/auth/angular";
|
||||
import {
|
||||
PinServiceAbstraction,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
import { DeviceType } from "@bitwarden/common/enums";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { BiometricsService } from "@bitwarden/key-management";
|
||||
|
||||
import { DesktopLockComponentService } from "./desktop-lock-component.service";
|
||||
|
||||
// ipc mock global
|
||||
const isWindowVisibleMock = jest.fn();
|
||||
const biometricEnabledMock = jest.fn();
|
||||
(global as any).ipc = {
|
||||
keyManagement: {
|
||||
biometric: {
|
||||
enabled: biometricEnabledMock,
|
||||
},
|
||||
},
|
||||
platform: {
|
||||
isWindowVisible: isWindowVisibleMock,
|
||||
},
|
||||
};
|
||||
|
||||
describe("DesktopLockComponentService", () => {
|
||||
let service: DesktopLockComponentService;
|
||||
|
||||
let userDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
|
||||
let platformUtilsService: MockProxy<PlatformUtilsService>;
|
||||
let biometricsService: MockProxy<BiometricsService>;
|
||||
let pinService: MockProxy<PinServiceAbstraction>;
|
||||
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
|
||||
let cryptoService: MockProxy<CryptoService>;
|
||||
|
||||
beforeEach(() => {
|
||||
userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
|
||||
platformUtilsService = mock<PlatformUtilsService>();
|
||||
biometricsService = mock<BiometricsService>();
|
||||
pinService = mock<PinServiceAbstraction>();
|
||||
vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
|
||||
cryptoService = mock<CryptoService>();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
DesktopLockComponentService,
|
||||
{
|
||||
provide: UserDecryptionOptionsServiceAbstraction,
|
||||
useValue: userDecryptionOptionsService,
|
||||
},
|
||||
{
|
||||
provide: PlatformUtilsService,
|
||||
useValue: platformUtilsService,
|
||||
},
|
||||
{
|
||||
provide: BiometricsService,
|
||||
useValue: biometricsService,
|
||||
},
|
||||
{
|
||||
provide: PinServiceAbstraction,
|
||||
useValue: pinService,
|
||||
},
|
||||
{
|
||||
provide: VaultTimeoutSettingsService,
|
||||
useValue: vaultTimeoutSettingsService,
|
||||
},
|
||||
{
|
||||
provide: CryptoService,
|
||||
useValue: cryptoService,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.inject(DesktopLockComponentService);
|
||||
});
|
||||
|
||||
it("instantiates", () => {
|
||||
expect(service).not.toBeFalsy();
|
||||
});
|
||||
|
||||
// getBiometricsError
|
||||
describe("getBiometricsError", () => {
|
||||
it("returns null when given null", () => {
|
||||
const result = service.getBiometricsError(null);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when given an unknown error", () => {
|
||||
const result = service.getBiometricsError({ message: "unknown" });
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getPreviousUrl", () => {
|
||||
it("returns null", () => {
|
||||
const result = service.getPreviousUrl();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("isWindowVisible", () => {
|
||||
it("returns the window visibility", async () => {
|
||||
isWindowVisibleMock.mockReturnValue(true);
|
||||
const result = await service.isWindowVisible();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBiometricsUnlockBtnText", () => {
|
||||
it("returns the correct text for Mac OS", () => {
|
||||
platformUtilsService.getDevice.mockReturnValue(DeviceType.MacOsDesktop);
|
||||
const result = service.getBiometricsUnlockBtnText();
|
||||
expect(result).toBe("unlockWithTouchId");
|
||||
});
|
||||
|
||||
it("returns the correct text for Windows", () => {
|
||||
platformUtilsService.getDevice.mockReturnValue(DeviceType.WindowsDesktop);
|
||||
const result = service.getBiometricsUnlockBtnText();
|
||||
expect(result).toBe("unlockWithWindowsHello");
|
||||
});
|
||||
|
||||
it("returns the correct text for Linux", () => {
|
||||
platformUtilsService.getDevice.mockReturnValue(DeviceType.LinuxDesktop);
|
||||
const result = service.getBiometricsUnlockBtnText();
|
||||
expect(result).toBe("unlockWithPolkit");
|
||||
});
|
||||
|
||||
it("throws an error for an unsupported platform", () => {
|
||||
platformUtilsService.getDevice.mockReturnValue("unsupported" as any);
|
||||
expect(() => service.getBiometricsUnlockBtnText()).toThrowError("Unsupported platform");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAvailableUnlockOptions$", () => {
|
||||
interface MockInputs {
|
||||
hasMasterPassword: boolean;
|
||||
osSupportsBiometric: boolean;
|
||||
biometricLockSet: boolean;
|
||||
biometricReady: boolean;
|
||||
hasBiometricEncryptedUserKeyStored: boolean;
|
||||
platformSupportsSecureStorage: boolean;
|
||||
pinDecryptionAvailable: boolean;
|
||||
}
|
||||
|
||||
const table: [MockInputs, UnlockOptions][] = [
|
||||
[
|
||||
// MP + PIN + Biometrics available
|
||||
{
|
||||
hasMasterPassword: true,
|
||||
osSupportsBiometric: true,
|
||||
biometricLockSet: true,
|
||||
hasBiometricEncryptedUserKeyStored: true,
|
||||
biometricReady: true,
|
||||
platformSupportsSecureStorage: true,
|
||||
pinDecryptionAvailable: true,
|
||||
},
|
||||
{
|
||||
masterPassword: {
|
||||
enabled: true,
|
||||
},
|
||||
pin: {
|
||||
enabled: true,
|
||||
},
|
||||
biometrics: {
|
||||
enabled: true,
|
||||
disableReason: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
// PIN + Biometrics available
|
||||
{
|
||||
hasMasterPassword: false,
|
||||
osSupportsBiometric: true,
|
||||
biometricLockSet: true,
|
||||
hasBiometricEncryptedUserKeyStored: true,
|
||||
biometricReady: true,
|
||||
platformSupportsSecureStorage: true,
|
||||
pinDecryptionAvailable: true,
|
||||
},
|
||||
{
|
||||
masterPassword: {
|
||||
enabled: false,
|
||||
},
|
||||
pin: {
|
||||
enabled: true,
|
||||
},
|
||||
biometrics: {
|
||||
enabled: true,
|
||||
disableReason: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
// Biometrics available: user key stored with no secure storage
|
||||
{
|
||||
hasMasterPassword: false,
|
||||
osSupportsBiometric: true,
|
||||
biometricLockSet: true,
|
||||
hasBiometricEncryptedUserKeyStored: true,
|
||||
biometricReady: true,
|
||||
platformSupportsSecureStorage: false,
|
||||
pinDecryptionAvailable: false,
|
||||
},
|
||||
{
|
||||
masterPassword: {
|
||||
enabled: false,
|
||||
},
|
||||
pin: {
|
||||
enabled: false,
|
||||
},
|
||||
biometrics: {
|
||||
enabled: true,
|
||||
disableReason: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
// Biometrics available: no user key stored with no secure storage
|
||||
{
|
||||
hasMasterPassword: false,
|
||||
osSupportsBiometric: true,
|
||||
biometricLockSet: true,
|
||||
hasBiometricEncryptedUserKeyStored: false,
|
||||
biometricReady: true,
|
||||
platformSupportsSecureStorage: false,
|
||||
pinDecryptionAvailable: false,
|
||||
},
|
||||
{
|
||||
masterPassword: {
|
||||
enabled: false,
|
||||
},
|
||||
pin: {
|
||||
enabled: false,
|
||||
},
|
||||
biometrics: {
|
||||
enabled: true,
|
||||
disableReason: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
// Biometrics not available: biometric not ready
|
||||
{
|
||||
hasMasterPassword: false,
|
||||
osSupportsBiometric: true,
|
||||
biometricLockSet: true,
|
||||
hasBiometricEncryptedUserKeyStored: true,
|
||||
biometricReady: false,
|
||||
platformSupportsSecureStorage: true,
|
||||
pinDecryptionAvailable: false,
|
||||
},
|
||||
{
|
||||
masterPassword: {
|
||||
enabled: false,
|
||||
},
|
||||
pin: {
|
||||
enabled: false,
|
||||
},
|
||||
biometrics: {
|
||||
enabled: false,
|
||||
disableReason: BiometricsDisableReason.SystemBiometricsUnavailable,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
// Biometrics not available: biometric lock not set
|
||||
{
|
||||
hasMasterPassword: false,
|
||||
osSupportsBiometric: true,
|
||||
biometricLockSet: false,
|
||||
hasBiometricEncryptedUserKeyStored: true,
|
||||
biometricReady: true,
|
||||
platformSupportsSecureStorage: true,
|
||||
pinDecryptionAvailable: false,
|
||||
},
|
||||
{
|
||||
masterPassword: {
|
||||
enabled: false,
|
||||
},
|
||||
pin: {
|
||||
enabled: false,
|
||||
},
|
||||
biometrics: {
|
||||
enabled: false,
|
||||
disableReason: BiometricsDisableReason.EncryptedKeysUnavailable,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
// Biometrics not available: user key not stored
|
||||
{
|
||||
hasMasterPassword: false,
|
||||
osSupportsBiometric: true,
|
||||
biometricLockSet: true,
|
||||
hasBiometricEncryptedUserKeyStored: false,
|
||||
biometricReady: true,
|
||||
platformSupportsSecureStorage: true,
|
||||
pinDecryptionAvailable: false,
|
||||
},
|
||||
{
|
||||
masterPassword: {
|
||||
enabled: false,
|
||||
},
|
||||
pin: {
|
||||
enabled: false,
|
||||
},
|
||||
biometrics: {
|
||||
enabled: false,
|
||||
disableReason: BiometricsDisableReason.EncryptedKeysUnavailable,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
// Biometrics not available: OS doesn't support
|
||||
{
|
||||
hasMasterPassword: false,
|
||||
osSupportsBiometric: false,
|
||||
biometricLockSet: true,
|
||||
hasBiometricEncryptedUserKeyStored: true,
|
||||
biometricReady: true,
|
||||
platformSupportsSecureStorage: true,
|
||||
pinDecryptionAvailable: false,
|
||||
},
|
||||
{
|
||||
masterPassword: {
|
||||
enabled: false,
|
||||
},
|
||||
pin: {
|
||||
enabled: false,
|
||||
},
|
||||
biometrics: {
|
||||
enabled: false,
|
||||
disableReason: BiometricsDisableReason.NotSupportedOnOperatingSystem,
|
||||
},
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
test.each(table)("returns unlock options", async (mockInputs, expectedOutput) => {
|
||||
const userId = "userId" as UserId;
|
||||
const userDecryptionOptions = {
|
||||
hasMasterPassword: mockInputs.hasMasterPassword,
|
||||
};
|
||||
|
||||
// MP
|
||||
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
|
||||
of(userDecryptionOptions),
|
||||
);
|
||||
|
||||
// Biometrics
|
||||
biometricsService.supportsBiometric.mockResolvedValue(mockInputs.osSupportsBiometric);
|
||||
vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(mockInputs.biometricLockSet);
|
||||
cryptoService.hasUserKeyStored.mockResolvedValue(
|
||||
mockInputs.hasBiometricEncryptedUserKeyStored,
|
||||
);
|
||||
platformUtilsService.supportsSecureStorage.mockReturnValue(
|
||||
mockInputs.platformSupportsSecureStorage,
|
||||
);
|
||||
biometricEnabledMock.mockResolvedValue(mockInputs.biometricReady);
|
||||
|
||||
// PIN
|
||||
pinService.isPinDecryptionAvailable.mockResolvedValue(mockInputs.pinDecryptionAvailable);
|
||||
|
||||
const unlockOptions = await firstValueFrom(service.getAvailableUnlockOptions$(userId));
|
||||
|
||||
expect(unlockOptions).toEqual(expectedOutput);
|
||||
});
|
||||
});
|
||||
});
|
||||
129
apps/desktop/src/services/desktop-lock-component.service.ts
Normal file
129
apps/desktop/src/services/desktop-lock-component.service.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { inject } from "@angular/core";
|
||||
import { combineLatest, defer, map, Observable } from "rxjs";
|
||||
|
||||
import {
|
||||
BiometricsDisableReason,
|
||||
LockComponentService,
|
||||
UnlockOptions,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import {
|
||||
PinServiceAbstraction,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
import { DeviceType } from "@bitwarden/common/enums";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { KeySuffixOptions } from "@bitwarden/common/platform/enums";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { BiometricsService } from "@bitwarden/key-management";
|
||||
|
||||
export class DesktopLockComponentService implements LockComponentService {
|
||||
private readonly userDecryptionOptionsService = inject(UserDecryptionOptionsServiceAbstraction);
|
||||
private readonly platformUtilsService = inject(PlatformUtilsService);
|
||||
private readonly biometricsService = inject(BiometricsService);
|
||||
private readonly pinService = inject(PinServiceAbstraction);
|
||||
private readonly vaultTimeoutSettingsService = inject(VaultTimeoutSettingsService);
|
||||
private readonly cryptoService = inject(CryptoService);
|
||||
|
||||
constructor() {}
|
||||
|
||||
getBiometricsError(error: any): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
getPreviousUrl(): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
async isWindowVisible(): Promise<boolean> {
|
||||
return ipc.platform.isWindowVisible();
|
||||
}
|
||||
|
||||
getBiometricsUnlockBtnText(): string {
|
||||
switch (this.platformUtilsService.getDevice()) {
|
||||
case DeviceType.MacOsDesktop:
|
||||
return "unlockWithTouchId";
|
||||
case DeviceType.WindowsDesktop:
|
||||
return "unlockWithWindowsHello";
|
||||
case DeviceType.LinuxDesktop:
|
||||
return "unlockWithPolkit";
|
||||
default:
|
||||
throw new Error("Unsupported platform");
|
||||
}
|
||||
}
|
||||
|
||||
private async isBiometricLockSet(userId: UserId): Promise<boolean> {
|
||||
const biometricLockSet = await this.vaultTimeoutSettingsService.isBiometricLockSet(userId);
|
||||
const hasBiometricEncryptedUserKeyStored = await this.cryptoService.hasUserKeyStored(
|
||||
KeySuffixOptions.Biometric,
|
||||
userId,
|
||||
);
|
||||
const platformSupportsSecureStorage = this.platformUtilsService.supportsSecureStorage();
|
||||
|
||||
return (
|
||||
biometricLockSet && (hasBiometricEncryptedUserKeyStored || !platformSupportsSecureStorage)
|
||||
);
|
||||
}
|
||||
|
||||
private async isBiometricsSupportedAndReady(
|
||||
userId: UserId,
|
||||
): Promise<{ supportsBiometric: boolean; biometricReady: boolean }> {
|
||||
const supportsBiometric = await this.biometricsService.supportsBiometric();
|
||||
const biometricReady = await ipc.keyManagement.biometric.enabled(userId);
|
||||
return { supportsBiometric, biometricReady };
|
||||
}
|
||||
|
||||
getAvailableUnlockOptions$(userId: UserId): Observable<UnlockOptions> {
|
||||
return combineLatest([
|
||||
// Note: defer is preferable b/c it delays the execution of the function until the observable is subscribed to
|
||||
defer(() => this.isBiometricsSupportedAndReady(userId)),
|
||||
defer(() => this.isBiometricLockSet(userId)),
|
||||
this.userDecryptionOptionsService.userDecryptionOptionsById$(userId),
|
||||
defer(() => this.pinService.isPinDecryptionAvailable(userId)),
|
||||
]).pipe(
|
||||
map(
|
||||
([biometricsData, isBiometricsLockSet, userDecryptionOptions, pinDecryptionAvailable]) => {
|
||||
const disableReason = this.getBiometricsDisabledReason(
|
||||
biometricsData.supportsBiometric,
|
||||
isBiometricsLockSet,
|
||||
biometricsData.biometricReady,
|
||||
);
|
||||
|
||||
const unlockOpts: UnlockOptions = {
|
||||
masterPassword: {
|
||||
enabled: userDecryptionOptions.hasMasterPassword,
|
||||
},
|
||||
pin: {
|
||||
enabled: pinDecryptionAvailable,
|
||||
},
|
||||
biometrics: {
|
||||
enabled:
|
||||
biometricsData.supportsBiometric &&
|
||||
isBiometricsLockSet &&
|
||||
biometricsData.biometricReady,
|
||||
disableReason: disableReason,
|
||||
},
|
||||
};
|
||||
|
||||
return unlockOpts;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private getBiometricsDisabledReason(
|
||||
osSupportsBiometric: boolean,
|
||||
biometricLockSet: boolean,
|
||||
biometricReady: boolean,
|
||||
): BiometricsDisableReason | null {
|
||||
if (!osSupportsBiometric) {
|
||||
return BiometricsDisableReason.NotSupportedOnOperatingSystem;
|
||||
} else if (!biometricLockSet) {
|
||||
return BiometricsDisableReason.EncryptedKeysUnavailable;
|
||||
} else if (!biometricReady) {
|
||||
return BiometricsDisableReason.SystemBiometricsUnavailable;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import { CollectionAccessSelectionView } from "@bitwarden/admin-console/common";
|
||||
import { View } from "@bitwarden/common/src/models/view/view";
|
||||
|
||||
import { GroupDetailsResponse, GroupResponse } from "../services/group/responses/group.response";
|
||||
|
||||
import { CollectionAccessSelectionView } from "./collection-access-selection.view";
|
||||
|
||||
export class GroupView implements View {
|
||||
id: string;
|
||||
organizationId: string;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from "./collection-access-selection.view";
|
||||
export * from "./group.view";
|
||||
export * from "./organization-user.view";
|
||||
export * from "./organization-user-admin-view";
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { CollectionAccessSelectionView } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
OrganizationUserStatusType,
|
||||
OrganizationUserType,
|
||||
} from "@bitwarden/common/admin-console/enums";
|
||||
import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api";
|
||||
|
||||
import { CollectionAccessSelectionView } from "./collection-access-selection.view";
|
||||
|
||||
export class OrganizationUserAdminView {
|
||||
id: string;
|
||||
userId: string;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { OrganizationUserUserDetailsResponse } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
OrganizationUserUserDetailsResponse,
|
||||
CollectionAccessSelectionView,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
OrganizationUserStatusType,
|
||||
OrganizationUserType,
|
||||
} from "@bitwarden/common/admin-console/enums";
|
||||
import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api";
|
||||
|
||||
import { CollectionAccessSelectionView } from "./collection-access-selection.view";
|
||||
|
||||
export class OrganizationUserView {
|
||||
id: string;
|
||||
userId: string;
|
||||
|
||||
@@ -14,7 +14,11 @@ import {
|
||||
takeUntil,
|
||||
} from "rxjs";
|
||||
|
||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
CollectionAdminService,
|
||||
CollectionAdminView,
|
||||
OrganizationUserApiService,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
@@ -26,8 +30,6 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { CollectionAdminService } from "../../../vault/core/collection-admin.service";
|
||||
import { CollectionAdminView } from "../../../vault/core/views/collection-admin.view";
|
||||
import { InternalGroupService as GroupService, GroupView } from "../core";
|
||||
import {
|
||||
AccessItemType,
|
||||
|
||||
@@ -13,7 +13,12 @@ import {
|
||||
takeUntil,
|
||||
} from "rxjs";
|
||||
|
||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
CollectionAccessSelectionView,
|
||||
CollectionAdminService,
|
||||
CollectionAdminView,
|
||||
OrganizationUserApiService,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import {
|
||||
OrganizationUserStatusType,
|
||||
@@ -24,14 +29,10 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { CollectionAdminService } from "../../../../../vault/core/collection-admin.service";
|
||||
import { CollectionAdminView } from "../../../../../vault/core/views/collection-admin.view";
|
||||
import {
|
||||
CollectionAccessSelectionView,
|
||||
GroupService,
|
||||
GroupView,
|
||||
OrganizationUserAdminView,
|
||||
@@ -133,7 +134,6 @@ export class MemberDialogComponent implements OnDestroy {
|
||||
@Inject(DIALOG_DATA) protected params: MemberDialogParams,
|
||||
private dialogRef: DialogRef<MemberDialogResult>,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private formBuilder: FormBuilder,
|
||||
// TODO: We should really look into consolidating naming conventions for these services
|
||||
private collectionAdminService: CollectionAdminService,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { CollectionAdminService } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
canAccessVaultTab,
|
||||
OrganizationService,
|
||||
@@ -11,7 +12,6 @@ import { ImportComponent } from "@bitwarden/importer/ui";
|
||||
|
||||
import { LooseComponentsModule, SharedModule } from "../../../shared";
|
||||
import { ImportCollectionAdminService } from "../../../tools/import/import-collection-admin.service";
|
||||
import { CollectionAdminService } from "../../../vault/core/collection-admin.service";
|
||||
|
||||
@Component({
|
||||
templateUrl: "org-import.component.html",
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { OrganizationUserUserDetailsResponse } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
CollectionAccessSelectionView,
|
||||
OrganizationUserUserDetailsResponse,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
OrganizationUserStatusType,
|
||||
OrganizationUserType,
|
||||
} from "@bitwarden/common/admin-console/enums";
|
||||
import { SelectItemView } from "@bitwarden/components";
|
||||
|
||||
import { CollectionAccessSelectionView, GroupView } from "../../../core";
|
||||
import { GroupView } from "../../../core";
|
||||
|
||||
/**
|
||||
* Permission options that replace/correspond with manage, readOnly, and hidePassword server fields.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./webauthn-login";
|
||||
export * from "./set-password-jit";
|
||||
export * from "./registration";
|
||||
export * from "./web-lock-component.service";
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { WebLockComponentService } from "./web-lock-component.service";
|
||||
|
||||
describe("WebLockComponentService", () => {
|
||||
let service: WebLockComponentService;
|
||||
|
||||
let userDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
|
||||
|
||||
beforeEach(() => {
|
||||
userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
WebLockComponentService,
|
||||
{
|
||||
provide: UserDecryptionOptionsServiceAbstraction,
|
||||
useValue: userDecryptionOptionsService,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.inject(WebLockComponentService);
|
||||
});
|
||||
|
||||
it("instantiates", () => {
|
||||
expect(service).not.toBeFalsy();
|
||||
});
|
||||
|
||||
describe("getBiometricsError", () => {
|
||||
it("throws an error when given a null input", () => {
|
||||
expect(() => service.getBiometricsError(null)).toThrow(
|
||||
"Biometric unlock is not supported in the web app. See getAvailableUnlockOptions$",
|
||||
);
|
||||
});
|
||||
it("throws an error when given a non-null input", () => {
|
||||
expect(() => service.getBiometricsError("error")).toThrow(
|
||||
"Biometric unlock is not supported in the web app. See getAvailableUnlockOptions$",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getPreviousUrl", () => {
|
||||
it("returns null", () => {
|
||||
expect(service.getPreviousUrl()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("isWindowVisible", () => {
|
||||
it("throws an error", async () => {
|
||||
await expect(service.isWindowVisible()).rejects.toThrow("Method not implemented.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBiometricsUnlockBtnText", () => {
|
||||
it("throws an error", () => {
|
||||
expect(() => service.getBiometricsUnlockBtnText()).toThrow(
|
||||
"Biometric unlock is not supported in the web app. See getAvailableUnlockOptions$",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAvailableUnlockOptions$", () => {
|
||||
it("returns an observable of unlock options", async () => {
|
||||
const userId = "user-id" as UserId;
|
||||
const userDecryptionOptions = {
|
||||
hasMasterPassword: true,
|
||||
};
|
||||
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValueOnce(
|
||||
of(userDecryptionOptions),
|
||||
);
|
||||
|
||||
const unlockOptions = await firstValueFrom(service.getAvailableUnlockOptions$(userId));
|
||||
|
||||
expect(unlockOptions).toEqual({
|
||||
masterPassword: {
|
||||
enabled: true,
|
||||
},
|
||||
pin: {
|
||||
enabled: false,
|
||||
},
|
||||
biometrics: {
|
||||
enabled: false,
|
||||
disableReason: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import { inject } from "@angular/core";
|
||||
import { map, Observable } from "rxjs";
|
||||
|
||||
import { LockComponentService, UnlockOptions } from "@bitwarden/auth/angular";
|
||||
import {
|
||||
UserDecryptionOptions,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
export class WebLockComponentService implements LockComponentService {
|
||||
private readonly userDecryptionOptionsService = inject(UserDecryptionOptionsServiceAbstraction);
|
||||
|
||||
constructor() {}
|
||||
|
||||
getBiometricsError(error: any): string | null {
|
||||
throw new Error(
|
||||
"Biometric unlock is not supported in the web app. See getAvailableUnlockOptions$",
|
||||
);
|
||||
}
|
||||
|
||||
getPreviousUrl(): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
async isWindowVisible(): Promise<boolean> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
getBiometricsUnlockBtnText(): string {
|
||||
throw new Error(
|
||||
"Biometric unlock is not supported in the web app. See getAvailableUnlockOptions$",
|
||||
);
|
||||
}
|
||||
|
||||
getAvailableUnlockOptions$(userId: UserId): Observable<UnlockOptions> {
|
||||
return this.userDecryptionOptionsService.userDecryptionOptionsById$(userId).pipe(
|
||||
map((userDecryptionOptions: UserDecryptionOptions) => {
|
||||
const unlockOpts: UnlockOptions = {
|
||||
masterPassword: {
|
||||
enabled: userDecryptionOptions.hasMasterPassword,
|
||||
},
|
||||
pin: {
|
||||
enabled: false,
|
||||
},
|
||||
biometrics: {
|
||||
enabled: false,
|
||||
disableReason: null,
|
||||
},
|
||||
};
|
||||
return unlockOpts;
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog>
|
||||
<h1 bitDialogTitle>
|
||||
{{ (hasBillingToken ? "viewBillingSyncToken" : "generateBillingSyncToken") | i18n }}
|
||||
{{ (hasBillingToken ? "viewBillingToken" : "generateBillingToken") | i18n }}
|
||||
</h1>
|
||||
<div bitDialogContent>
|
||||
<app-user-verification formControlName="verification" *ngIf="!clientSecret">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog>
|
||||
<h1 bitDialogTitle>
|
||||
{{ "manageBillingSync" | i18n }}
|
||||
{{ "manageBillingTokenSync" | i18n }}
|
||||
</h1>
|
||||
<div bitDialogContent>
|
||||
<p>{{ "billingSyncKeyDesc" | i18n }}</p>
|
||||
|
||||
@@ -280,7 +280,7 @@
|
||||
(click)="manageBillingSync()"
|
||||
*ngIf="canManageBillingSync"
|
||||
>
|
||||
{{ (hasBillingSyncToken ? "manageBillingSync" : "setUpBillingSync") | i18n }}
|
||||
{{ (hasBillingSyncToken ? "viewBillingToken" : "setUpBillingSync") | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
<ng-container *ngIf="userOrg.canEditSubscription">
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
</a>
|
||||
</bit-label>
|
||||
<bit-hint>
|
||||
{{ "billingSyncDesc" | i18n }}
|
||||
{{ "automaticBillingSyncDesc" | i18n }}
|
||||
</bit-hint>
|
||||
</bit-radio-button>
|
||||
<ng-container *ngIf="updateMethod === licenseOptions.SYNC">
|
||||
@@ -100,7 +100,7 @@
|
||||
type="button"
|
||||
(click)="manageBillingSyncSelfHosted()"
|
||||
>
|
||||
{{ "manageBillingSync" | i18n }}
|
||||
{{ "manageBillingTokenSync" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
bitButton
|
||||
@@ -122,7 +122,7 @@
|
||||
>
|
||||
<bit-label>{{ "manualUpload" | i18n }}</bit-label>
|
||||
<bit-hint>
|
||||
{{ "manualUploadDesc" | i18n }}
|
||||
{{ "manualBillingTokenUploadDesc" | i18n }}
|
||||
</bit-hint>
|
||||
</bit-radio-button>
|
||||
<ng-container *ngIf="updateMethod === licenseOptions.UPLOAD">
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { APP_INITIALIZER, NgModule, Optional, SkipSelf } from "@angular/core";
|
||||
|
||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
CollectionAdminService,
|
||||
DefaultCollectionAdminService,
|
||||
OrganizationUserApiService,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
|
||||
import {
|
||||
CLIENT_TYPE,
|
||||
@@ -20,6 +24,7 @@ import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.
|
||||
import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/services/modal.service";
|
||||
import {
|
||||
RegistrationFinishService as RegistrationFinishServiceAbstraction,
|
||||
LockComponentService,
|
||||
SetPasswordJitService,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
@@ -59,10 +64,15 @@ import {
|
||||
ThemeStateService,
|
||||
} from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
import { VaultTimeout, VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type";
|
||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||
import { BiometricsService } from "@bitwarden/key-management";
|
||||
|
||||
import { PolicyListService } from "../admin-console/core/policy-list.service";
|
||||
import { WebRegistrationFinishService, WebSetPasswordJitService } from "../auth";
|
||||
import {
|
||||
WebSetPasswordJitService,
|
||||
WebRegistrationFinishService,
|
||||
WebLockComponentService,
|
||||
} from "../auth";
|
||||
import { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service";
|
||||
import { HtmlStorageService } from "../core/html-storage.service";
|
||||
import { I18nService } from "../core/i18n.service";
|
||||
@@ -70,7 +80,6 @@ import { WebBiometricsService } from "../key-management/web-biometric.service";
|
||||
import { WebEnvironmentService } from "../platform/web-environment.service";
|
||||
import { WebMigrationRunner } from "../platform/web-migration-runner";
|
||||
import { WebStorageServiceProvider } from "../platform/web-storage-service.provider";
|
||||
import { CollectionAdminService } from "../vault/core/collection-admin.service";
|
||||
|
||||
import { EventService } from "./event.service";
|
||||
import { InitService } from "./init.service";
|
||||
@@ -144,7 +153,6 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: WebFileDownloadService,
|
||||
useAngularDecorators: true,
|
||||
}),
|
||||
safeProvider(CollectionAdminService),
|
||||
safeProvider({
|
||||
provide: WindowStorageService,
|
||||
useFactory: () => new WindowStorageService(window.localStorage),
|
||||
@@ -197,6 +205,11 @@ const safeProviders: SafeProvider[] = [
|
||||
PolicyService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: LockComponentService,
|
||||
useClass: WebLockComponentService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SetPasswordJitService,
|
||||
useClass: WebSetPasswordJitService,
|
||||
@@ -217,6 +230,11 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: DefaultAppIdService,
|
||||
deps: [OBSERVABLE_DISK_LOCAL_STORAGE, LogService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: CollectionAdminService,
|
||||
useClass: DefaultCollectionAdminService,
|
||||
deps: [ApiService, CryptoServiceAbstraction, EncryptService, CollectionService],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
unauthGuardFn,
|
||||
} from "@bitwarden/angular/auth/guards";
|
||||
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
|
||||
import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh-swap";
|
||||
import {
|
||||
AnonLayoutWrapperComponent,
|
||||
AnonLayoutWrapperData,
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
RegistrationStartSecondaryComponentData,
|
||||
SetPasswordJitComponent,
|
||||
RegistrationLinkExpiredComponent,
|
||||
LockV2Component,
|
||||
LockIcon,
|
||||
UserLockIcon,
|
||||
} from "@bitwarden/auth/angular";
|
||||
@@ -337,21 +339,41 @@ const routes: Routes = [
|
||||
pageTitle: "logIn",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "lock",
|
||||
canActivate: [deepLinkGuard(), lockGuard()],
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: LockComponent,
|
||||
},
|
||||
],
|
||||
data: {
|
||||
pageTitle: "yourVaultIsLockedV2",
|
||||
pageIcon: LockIcon,
|
||||
showReadonlyHostname: true,
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
},
|
||||
...extensionRefreshSwap(
|
||||
LockComponent,
|
||||
LockV2Component,
|
||||
{
|
||||
path: "lock",
|
||||
canActivate: [deepLinkGuard(), lockGuard()],
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: LockComponent,
|
||||
},
|
||||
],
|
||||
data: {
|
||||
pageTitle: "yourVaultIsLockedV2",
|
||||
pageIcon: LockIcon,
|
||||
showReadonlyHostname: true,
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
},
|
||||
{
|
||||
path: "lock",
|
||||
canActivate: [deepLinkGuard(), lockGuard()],
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: LockV2Component,
|
||||
},
|
||||
],
|
||||
data: {
|
||||
pageTitle: "yourAccountIsLocked",
|
||||
pageIcon: LockIcon,
|
||||
showReadonlyHostname: true,
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
},
|
||||
),
|
||||
|
||||
{
|
||||
path: "2fa",
|
||||
canActivate: [unauthGuardFn()],
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { CollectionAdminService, CollectionAdminView } from "@bitwarden/admin-console/common";
|
||||
|
||||
import { ImportCollectionServiceAbstraction } from "../../../../../../libs/importer/src/services/import-collection.service.abstraction";
|
||||
import { CollectionAdminService } from "../../vault/core/collection-admin.service";
|
||||
import { CollectionAdminView } from "../../vault/core/views/collection-admin.view";
|
||||
|
||||
@Injectable()
|
||||
export class ImportCollectionAdminService implements ImportCollectionServiceAbstraction {
|
||||
|
||||
@@ -14,6 +14,9 @@ import {
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import {
|
||||
CollectionAccessSelectionView,
|
||||
CollectionAdminService,
|
||||
CollectionAdminView,
|
||||
OrganizationUserApiService,
|
||||
OrganizationUserUserMiniResponse,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
@@ -26,11 +29,7 @@ import { CollectionResponse } from "@bitwarden/common/vault/models/response/coll
|
||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||
import { BitValidators, DialogService } from "@bitwarden/components";
|
||||
|
||||
import {
|
||||
CollectionAccessSelectionView,
|
||||
GroupService,
|
||||
GroupView,
|
||||
} from "../../../admin-console/organizations/core";
|
||||
import { GroupService, GroupView } from "../../../admin-console/organizations/core";
|
||||
import { PermissionMode } from "../../../admin-console/organizations/shared/components/access-selector/access-selector.component";
|
||||
import {
|
||||
AccessItemType,
|
||||
@@ -40,8 +39,6 @@ import {
|
||||
convertToPermission,
|
||||
convertToSelectionView,
|
||||
} from "../../../admin-console/organizations/shared/components/access-selector/access-selector.models";
|
||||
import { CollectionAdminService } from "../../core/collection-admin.service";
|
||||
import { CollectionAdminView } from "../../core/views/collection-admin.view";
|
||||
|
||||
export enum CollectionDialogTabType {
|
||||
Info = 0,
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
|
||||
import { CollectionAdminView, Unassigned } from "@bitwarden/admin-console/common";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||
|
||||
import { GroupView } from "../../../admin-console/organizations/core";
|
||||
import { CollectionAdminView } from "../../core/views/collection-admin.view";
|
||||
import { Unassigned } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model";
|
||||
|
||||
import {
|
||||
convertToPermission,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { SelectionModel } from "@angular/cdk/collections";
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
|
||||
import { Unassigned } from "@bitwarden/admin-console/common";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||
import { TableDataSource } from "@bitwarden/components";
|
||||
|
||||
import { GroupView } from "../../../admin-console/organizations/core";
|
||||
import { Unassigned } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model";
|
||||
|
||||
import { VaultItem } from "./vault-item";
|
||||
import { VaultItemEvent } from "./vault-item-event";
|
||||
|
||||
@@ -3,6 +3,11 @@ import { RouterModule } from "@angular/router";
|
||||
import { applicationConfig, Meta, moduleMetadata, StoryObj } from "@storybook/angular";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import {
|
||||
CollectionAccessSelectionView,
|
||||
CollectionAdminView,
|
||||
Unassigned,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { OrganizationUserType } from "@bitwarden/common/admin-console/enums";
|
||||
import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
@@ -19,13 +24,8 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
||||
|
||||
import {
|
||||
CollectionAccessSelectionView,
|
||||
GroupView,
|
||||
} from "../../../admin-console/organizations/core";
|
||||
import { GroupView } from "../../../admin-console/organizations/core";
|
||||
import { PreloadedEnglishI18nModule } from "../../../core/tests";
|
||||
import { CollectionAdminView } from "../../core/views/collection-admin.view";
|
||||
import { Unassigned } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model";
|
||||
|
||||
import { VaultItemsComponent } from "./vault-items.component";
|
||||
import { VaultItemsModule } from "./vault-items.module";
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { Component, Input, OnChanges } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { Unassigned } from "@bitwarden/admin-console/common";
|
||||
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
import { Unassigned } from "../vault-filter/shared/models/routed-vault-filter.model";
|
||||
|
||||
@Component({
|
||||
selector: "app-org-badge",
|
||||
templateUrl: "organization-name-badge.component.html",
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { CollectionAdminView } from "@bitwarden/admin-console/common";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { CollectionView } from "@bitwarden/common/src/vault/models/view/collection.view";
|
||||
import { FolderView } from "@bitwarden/common/src/vault/models/view/folder.view";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
|
||||
import { CollectionAdminView } from "../../../../core/views/collection-admin.view";
|
||||
import {
|
||||
CipherTypeFilter,
|
||||
CollectionFilter,
|
||||
|
||||
@@ -2,15 +2,12 @@ import { Injectable } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { combineLatest, map, Observable } from "rxjs";
|
||||
|
||||
import { Unassigned } from "@bitwarden/admin-console/common";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||
|
||||
import { RoutedVaultFilterBridge } from "../shared/models/routed-vault-filter-bridge.model";
|
||||
import {
|
||||
RoutedVaultFilterModel,
|
||||
Unassigned,
|
||||
All,
|
||||
} from "../shared/models/routed-vault-filter.model";
|
||||
import { RoutedVaultFilterModel, All } from "../shared/models/routed-vault-filter.model";
|
||||
import { VaultFilter } from "../shared/models/vault-filter.model";
|
||||
import {
|
||||
CipherTypeFilter,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
switchMap,
|
||||
} from "rxjs";
|
||||
|
||||
import { CollectionAdminView } from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
@@ -26,7 +27,6 @@ import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||
import { COLLAPSED_GROUPINGS } from "@bitwarden/common/vault/services/key-state/collapsed-groupings.state";
|
||||
|
||||
import { CollectionAdminView } from "../../../core/views/collection-admin.view";
|
||||
import {
|
||||
CipherTypeFilter,
|
||||
CollectionFilter,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Unassigned } from "@bitwarden/admin-console/common";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import { createFilterFunction } from "./filter-function";
|
||||
import { Unassigned, All } from "./routed-vault-filter.model";
|
||||
import { All } from "./routed-vault-filter.model";
|
||||
|
||||
describe("createFilter", () => {
|
||||
describe("given a generic cipher", () => {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Unassigned } from "@bitwarden/admin-console/common";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import { All, RoutedVaultFilterModel, Unassigned } from "./routed-vault-filter.model";
|
||||
import { All, RoutedVaultFilterModel } from "./routed-vault-filter.model";
|
||||
|
||||
export type FilterFunction = (cipher: CipherView) => boolean;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Unassigned } from "@bitwarden/admin-console/common";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
|
||||
@@ -8,7 +9,6 @@ import {
|
||||
isRoutedVaultFilterItemType,
|
||||
RoutedVaultFilterItemType,
|
||||
RoutedVaultFilterModel,
|
||||
Unassigned,
|
||||
} from "./routed-vault-filter.model";
|
||||
import { VaultFilter, VaultFilterFunction } from "./vault-filter.model";
|
||||
import {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
export const Unassigned = "unassigned";
|
||||
|
||||
export const All = "all";
|
||||
|
||||
// TODO: Remove `All` when moving to vertical navigation.
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { CollectionAdminView } from "@bitwarden/admin-console/common";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { FolderView } from "@bitwarden/common/src/vault/models/view/folder.view";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { ITreeNodeObject } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
|
||||
import { CollectionAdminView } from "../../../../core/views/collection-admin.view";
|
||||
|
||||
export type CipherStatus = "all" | "favorites" | "trash" | CipherType;
|
||||
|
||||
export type CipherTypeFilter = ITreeNodeObject & { type: CipherStatus; icon: string };
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { Unassigned } from "@bitwarden/admin-console/common";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
@@ -26,7 +27,6 @@ import { PipesModule } from "../pipes/pipes.module";
|
||||
import {
|
||||
All,
|
||||
RoutedVaultFilterModel,
|
||||
Unassigned,
|
||||
} from "../vault-filter/shared/models/routed-vault-filter.model";
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
tap,
|
||||
} from "rxjs/operators";
|
||||
|
||||
import { Unassigned } from "@bitwarden/admin-console/common";
|
||||
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
@@ -118,7 +119,6 @@ import { createFilterFunction } from "./vault-filter/shared/models/filter-functi
|
||||
import {
|
||||
All,
|
||||
RoutedVaultFilterModel,
|
||||
Unassigned,
|
||||
} from "./vault-filter/shared/models/routed-vault-filter.model";
|
||||
import { VaultFilter } from "./vault-filter/shared/models/vault-filter.model";
|
||||
import { FolderFilter, OrganizationFilter } from "./vault-filter/shared/models/vault-filter.type";
|
||||
|
||||
@@ -3,7 +3,10 @@ import { Component, Inject, OnDestroy } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { combineLatest, of, Subject, switchMap, takeUntil } from "rxjs";
|
||||
|
||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
CollectionAdminService,
|
||||
OrganizationUserApiService,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -23,7 +26,6 @@ import {
|
||||
PermissionMode,
|
||||
} from "../../../admin-console/organizations/shared/components/access-selector";
|
||||
import { SharedModule } from "../../../shared";
|
||||
import { CollectionAdminService } from "../../core/collection-admin.service";
|
||||
|
||||
export interface BulkCollectionsDialogParams {
|
||||
organizationId: string;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Injectable, OnDestroy } from "@angular/core";
|
||||
import { map, Observable, ReplaySubject, Subject } from "rxjs";
|
||||
|
||||
import { CollectionAdminView } from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -10,7 +11,6 @@ import { CollectionService } from "@bitwarden/common/vault/abstractions/collecti
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
|
||||
import { CollectionAdminView } from "../../../vault/core/views/collection-admin.view";
|
||||
import { VaultFilterService as BaseVaultFilterService } from "../../individual-vault/vault-filter/services/vault-filter.service";
|
||||
import { CollectionFilter } from "../../individual-vault/vault-filter/shared/models/vault-filter.type";
|
||||
|
||||
|
||||
@@ -3,6 +3,11 @@ import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import {
|
||||
CollectionAdminService,
|
||||
CollectionAdminView,
|
||||
Unassigned,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
@@ -22,13 +27,10 @@ import {
|
||||
|
||||
import { HeaderModule } from "../../../layouts/header/header.module";
|
||||
import { SharedModule } from "../../../shared";
|
||||
import { CollectionAdminView } from "../../../vault/core/views/collection-admin.view";
|
||||
import { CollectionDialogTabType } from "../../components/collection-dialog";
|
||||
import { CollectionAdminService } from "../../core/collection-admin.service";
|
||||
import {
|
||||
All,
|
||||
RoutedVaultFilterModel,
|
||||
Unassigned,
|
||||
} from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model";
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -30,6 +30,11 @@ import {
|
||||
withLatestFrom,
|
||||
} from "rxjs/operators";
|
||||
|
||||
import {
|
||||
CollectionAdminService,
|
||||
CollectionAdminView,
|
||||
Unassigned,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
@@ -72,8 +77,6 @@ import {
|
||||
} from "../components/collection-dialog";
|
||||
import { VaultItemEvent } from "../components/vault-items/vault-item-event";
|
||||
import { VaultItemsModule } from "../components/vault-items/vault-items.module";
|
||||
import { CollectionAdminService } from "../core/collection-admin.service";
|
||||
import { CollectionAdminView } from "../core/views/collection-admin.view";
|
||||
import {
|
||||
BulkDeleteDialogResult,
|
||||
openBulkDeleteDialog,
|
||||
@@ -85,7 +88,6 @@ import { createFilterFunction } from "../individual-vault/vault-filter/shared/mo
|
||||
import {
|
||||
All,
|
||||
RoutedVaultFilterModel,
|
||||
Unassigned,
|
||||
} from "../individual-vault/vault-filter/shared/models/routed-vault-filter.model";
|
||||
import {
|
||||
openViewCipherDialog,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CollectionAdminView } from "@bitwarden/admin-console/common";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import {
|
||||
CollectionView,
|
||||
@@ -5,8 +6,6 @@ import {
|
||||
} from "@bitwarden/common/vault/models/view/collection.view";
|
||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||
|
||||
import { CollectionAdminView } from "../../vault/core/views/collection-admin.view";
|
||||
|
||||
export function getNestedCollectionTree(
|
||||
collections: CollectionAdminView[],
|
||||
): TreeNode<CollectionAdminView>[];
|
||||
|
||||
@@ -1099,8 +1099,11 @@
|
||||
"yourVaultIsLockedV2": {
|
||||
"message": "Your vault is locked"
|
||||
},
|
||||
"uuid": {
|
||||
"message": "UUID"
|
||||
"yourAccountIsLocked": {
|
||||
"message": "Your account is locked"
|
||||
},
|
||||
"uuid":{
|
||||
"message" : "UUID"
|
||||
},
|
||||
"unlock": {
|
||||
"message": "Unlock"
|
||||
@@ -2564,6 +2567,9 @@
|
||||
"downloadLicense": {
|
||||
"message": "Download license"
|
||||
},
|
||||
"viewBillingToken": {
|
||||
"message": "View Billing Token"
|
||||
},
|
||||
"updateLicense": {
|
||||
"message": "Update license"
|
||||
},
|
||||
@@ -3169,6 +3175,10 @@
|
||||
"incorrectPin": {
|
||||
"message": "Incorrect PIN"
|
||||
},
|
||||
"pin": {
|
||||
"message": "PIN",
|
||||
"description": "PIN code. Ex. The short code (often numeric) that you use to unlock a device."
|
||||
},
|
||||
"exportedVault": {
|
||||
"message": "Vault exported"
|
||||
},
|
||||
@@ -5994,8 +6004,8 @@
|
||||
"viewBillingSyncToken": {
|
||||
"message": "View billing sync token"
|
||||
},
|
||||
"generateBillingSyncToken": {
|
||||
"message": "Generate billing sync token"
|
||||
"generateBillingToken": {
|
||||
"message": "Generate billing token"
|
||||
},
|
||||
"copyPasteBillingSync": {
|
||||
"message": "Copy and paste this token into the billing sync settings of your self-hosted organization."
|
||||
@@ -6003,8 +6013,8 @@
|
||||
"billingSyncCanAccess": {
|
||||
"message": "Your billing sync token can access and edit this organization's subscription settings."
|
||||
},
|
||||
"manageBillingSync": {
|
||||
"message": "Manage billing sync"
|
||||
"manageBillingTokenSync": {
|
||||
"message": "Manage Billing Token"
|
||||
},
|
||||
"setUpBillingSync": {
|
||||
"message": "Set up billing sync"
|
||||
@@ -6063,15 +6073,15 @@
|
||||
"billingSyncApiKeyRotated": {
|
||||
"message": "Token rotated"
|
||||
},
|
||||
"billingSyncDesc": {
|
||||
"message": "Billing sync unlocks Families sponsorships and automatic license syncing on your server. After making updates in the Bitwarden cloud server, select Sync License to apply changes."
|
||||
},
|
||||
"billingSyncKeyDesc": {
|
||||
"message": "A billing sync token from your cloud organization's subscription settings is required to complete this form."
|
||||
},
|
||||
"billingSyncKey": {
|
||||
"message": "Billing sync token"
|
||||
},
|
||||
"automaticBillingSyncDesc": {
|
||||
"message": "Automatic sync unlocks Families sponsorships and allows you to sync your license without uploading a file. After making updates in the Bitwarden cloud server, select Sync License to apply changes."
|
||||
},
|
||||
"active": {
|
||||
"message": "Active"
|
||||
},
|
||||
@@ -7463,6 +7473,15 @@
|
||||
"or": {
|
||||
"message": "or"
|
||||
},
|
||||
"unlockWithBiometrics": {
|
||||
"message": "Unlock with biometrics"
|
||||
},
|
||||
"unlockWithPin": {
|
||||
"message": "Unlock with PIN"
|
||||
},
|
||||
"unlockWithMasterPassword": {
|
||||
"message": "Unlock with master password"
|
||||
},
|
||||
"licenseAndBillingManagement": {
|
||||
"message": "License and billing management"
|
||||
},
|
||||
@@ -7472,11 +7491,11 @@
|
||||
"manualUpload": {
|
||||
"message": "Manual upload"
|
||||
},
|
||||
"manualUploadDesc": {
|
||||
"message": "If you do not want to opt into billing sync, manually upload your license here."
|
||||
"manualBillingTokenUploadDesc": {
|
||||
"message": "If you do not want to opt into billing sync, manually upload your license here. This will not automatically unlock Families sponsorships."
|
||||
},
|
||||
"syncLicense": {
|
||||
"message": "Sync license"
|
||||
"message": "Sync License"
|
||||
},
|
||||
"licenseSyncSuccess": {
|
||||
"message": "Successfully synced license"
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<app-danger-zone *ngIf="enableDeleteProvider$ | async">
|
||||
<app-danger-zone>
|
||||
<button type="button" bitButton buttonType="danger" (click)="deleteProvider()">
|
||||
{{ "deleteProvider" | i18n }}
|
||||
</button>
|
||||
|
||||
@@ -8,7 +8,6 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction";
|
||||
import { ProviderUpdateRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-update.request";
|
||||
import { ProviderResponse } from "@bitwarden/common/admin-console/models/response/provider/provider.response";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -33,9 +32,6 @@ export class AccountComponent implements OnDestroy, OnInit {
|
||||
providerName: ["" as ProviderResponse["name"]],
|
||||
providerBillingEmail: ["" as ProviderResponse["billingEmail"], Validators.email],
|
||||
});
|
||||
protected enableDeleteProvider$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.EnableDeleteProvider,
|
||||
);
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { CollectionAccessSelectionView } from "@bitwarden/admin-console/common";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CollectionAccessSelectionView } from "@bitwarden/web-vault/app/admin-console/organizations/core/views";
|
||||
import {
|
||||
getPermissionList,
|
||||
convertToPermission,
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { CollectionDetailsResponse } from "@bitwarden/common/vault/models/response/collection.response";
|
||||
|
||||
import { CollectionAccessSelectionView, CollectionAdminView } from "../models";
|
||||
|
||||
export abstract class CollectionAdminService {
|
||||
getAll: (organizationId: string) => Promise<CollectionAdminView[]>;
|
||||
get: (organizationId: string, collectionId: string) => Promise<CollectionAdminView | undefined>;
|
||||
save: (collection: CollectionAdminView) => Promise<CollectionDetailsResponse>;
|
||||
delete: (organizationId: string, collectionId: string) => Promise<void>;
|
||||
bulkAssignAccess: (
|
||||
organizationId: string,
|
||||
collectionIds: string[],
|
||||
users: CollectionAccessSelectionView[],
|
||||
groups: CollectionAccessSelectionView[],
|
||||
) => Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./collection-admin.service";
|
||||
3
libs/admin-console/src/common/collections/index.ts
Normal file
3
libs/admin-console/src/common/collections/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./abstractions";
|
||||
export * from "./models";
|
||||
export * from "./services";
|
||||
@@ -2,8 +2,9 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
|
||||
import { CollectionAccessDetailsResponse } from "@bitwarden/common/src/vault/models/response/collection.response";
|
||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||
|
||||
import { CollectionAccessSelectionView } from "../../../admin-console/organizations/core/views/collection-access-selection.view";
|
||||
import { Unassigned } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model";
|
||||
import { CollectionAccessSelectionView } from "../models";
|
||||
|
||||
export const Unassigned = "unassigned";
|
||||
|
||||
export class CollectionAdminView extends CollectionView {
|
||||
groups: CollectionAccessSelectionView[] = [];
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./bulk-collection-access.request";
|
||||
export * from "./collection-access-selection.view";
|
||||
export * from "./collection-admin.view";
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
@@ -14,13 +12,14 @@ import {
|
||||
CollectionResponse,
|
||||
} from "@bitwarden/common/vault/models/response/collection.response";
|
||||
|
||||
import { CollectionAccessSelectionView } from "../../admin-console/organizations/core";
|
||||
import { CollectionAdminService } from "../abstractions";
|
||||
import {
|
||||
BulkCollectionAccessRequest,
|
||||
CollectionAccessSelectionView,
|
||||
CollectionAdminView,
|
||||
} from "../models";
|
||||
|
||||
import { BulkCollectionAccessRequest } from "./bulk-collection-access.request";
|
||||
import { CollectionAdminView } from "./views/collection-admin.view";
|
||||
|
||||
@Injectable()
|
||||
export class CollectionAdminService {
|
||||
export class DefaultCollectionAdminService implements CollectionAdminService {
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private cryptoService: CryptoService,
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./default-collection-admin.service";
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./organization-user";
|
||||
export * from "./collections";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Directive, ElementRef, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { ActivatedRoute, NavigationSkipped, Router } from "@angular/router";
|
||||
import { Subject, firstValueFrom, of } from "rxjs";
|
||||
import { switchMap, take, takeUntil } from "rxjs/operators";
|
||||
|
||||
@@ -121,6 +121,14 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
// If the user navigates to /login from /login, reset the validatedEmail flag
|
||||
// This should bring the user back to the login screen with the email field
|
||||
this.router.events.pipe(takeUntil(this.destroy$)).subscribe((event) => {
|
||||
if (event instanceof NavigationSkipped && event.url === "/login") {
|
||||
this.validatedEmail = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Backup check to handle unknown case where activatedRoute is not available
|
||||
// This shouldn't happen under normal circumstances
|
||||
if (!this.route) {
|
||||
|
||||
@@ -38,6 +38,8 @@ export const authGuard: CanActivateFn = async (
|
||||
if (routerState != null) {
|
||||
messagingService.send("lockedUrl", { url: routerState.url });
|
||||
}
|
||||
// TODO PM-9674: when extension refresh is finished, remove promptBiometric
|
||||
// as it has been integrated into the component as a default feature.
|
||||
return router.createUrlTree(["lock"], { queryParams: { promptBiometric: true } });
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
'tw-min-h-[calc(100vh-54px)]': clientType === 'desktop',
|
||||
}"
|
||||
>
|
||||
<bit-icon *ngIf="!hideLogo" [icon]="logo" class="tw-w-[128px] [&>*]:tw-align-top"></bit-icon>
|
||||
<a *ngIf="!hideLogo" [routerLink]="['/']" class="tw-w-[128px] [&>*]:tw-align-top">
|
||||
<bit-icon [icon]="logo"></bit-icon>
|
||||
</a>
|
||||
|
||||
<div class="tw-text-center">
|
||||
<div class="tw-mx-auto tw-max-w-28 sm:tw-max-w-32">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Input, OnChanges, OnInit, SimpleChanges } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
@@ -15,7 +16,7 @@ import { BitwardenLogo, BitwardenShield } from "../icons";
|
||||
standalone: true,
|
||||
selector: "auth-anon-layout",
|
||||
templateUrl: "./anon-layout.component.html",
|
||||
imports: [IconModule, CommonModule, TypographyModule, SharedModule],
|
||||
imports: [IconModule, CommonModule, TypographyModule, SharedModule, RouterModule],
|
||||
})
|
||||
export class AnonLayoutComponent implements OnInit, OnChanges {
|
||||
@Input() title: string;
|
||||
|
||||
@@ -43,5 +43,9 @@ export * from "./registration/registration-env-selector/registration-env-selecto
|
||||
export * from "./registration/registration-finish/registration-finish.service";
|
||||
export * from "./registration/registration-finish/default-registration-finish.service";
|
||||
|
||||
// lock
|
||||
export * from "./lock/lock.component";
|
||||
export * from "./lock/lock-component.service";
|
||||
|
||||
// vault timeout
|
||||
export * from "./vault-timeout-input/vault-timeout-input.component";
|
||||
|
||||
48
libs/auth/src/angular/lock/lock-component.service.ts
Normal file
48
libs/auth/src/angular/lock/lock-component.service.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
export enum BiometricsDisableReason {
|
||||
NotSupportedOnOperatingSystem = "NotSupportedOnOperatingSystem",
|
||||
EncryptedKeysUnavailable = "BiometricsEncryptedKeysUnavailable",
|
||||
SystemBiometricsUnavailable = "SystemBiometricsUnavailable",
|
||||
}
|
||||
|
||||
// ex: type UnlockOptionValue = "masterPassword" | "pin" | "biometrics"
|
||||
export type UnlockOptionValue = (typeof UnlockOption)[keyof typeof UnlockOption];
|
||||
|
||||
export const UnlockOption = Object.freeze({
|
||||
MasterPassword: "masterPassword",
|
||||
Pin: "pin",
|
||||
Biometrics: "biometrics",
|
||||
}) satisfies { [Prop in keyof UnlockOptions as Capitalize<Prop>]: Prop };
|
||||
|
||||
export type UnlockOptions = {
|
||||
masterPassword: {
|
||||
enabled: boolean;
|
||||
};
|
||||
pin: {
|
||||
enabled: boolean;
|
||||
};
|
||||
biometrics: {
|
||||
enabled: boolean;
|
||||
disableReason: BiometricsDisableReason | null;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* The LockComponentService is a service which allows the single libs/auth LockComponent to delegate all
|
||||
* client specific functionality to client specific services implementations of LockComponentService.
|
||||
*/
|
||||
export abstract class LockComponentService {
|
||||
// Extension
|
||||
abstract getBiometricsError(error: any): string | null;
|
||||
abstract getPreviousUrl(): string | null;
|
||||
|
||||
// Desktop only
|
||||
abstract isWindowVisible(): Promise<boolean>;
|
||||
abstract getBiometricsUnlockBtnText(): string;
|
||||
|
||||
// Multi client
|
||||
abstract getAvailableUnlockOptions$(userId: UserId): Observable<UnlockOptions>;
|
||||
}
|
||||
191
libs/auth/src/angular/lock/lock.component.html
Normal file
191
libs/auth/src/angular/lock/lock.component.html
Normal file
@@ -0,0 +1,191 @@
|
||||
<ng-template #loading>
|
||||
<div class="tw-flex tw-items-center tw-justify-center" *ngIf="loading">
|
||||
<i class="bwi bwi-spinner bwi-spin bwi-3x" aria-hidden="true"></i>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-container *ngIf="unlockOptions; else loading">
|
||||
<!-- Biometrics Unlock -->
|
||||
<ng-container
|
||||
*ngIf="unlockOptions.biometrics.enabled && activeUnlockOption === UnlockOption.Biometrics"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
class="tw-mb-3"
|
||||
[disabled]="unlockingViaBiometrics"
|
||||
[loading]="unlockingViaBiometrics"
|
||||
block
|
||||
(click)="unlockViaBiometrics()"
|
||||
>
|
||||
<span> {{ biometricUnlockBtnText | i18n }}</span>
|
||||
</button>
|
||||
|
||||
<div class="tw-flex tw-flex-col tw-space-y-3">
|
||||
<p class="tw-text-center tw-mb-0">{{ "or" | i18n }}</p>
|
||||
|
||||
<ng-container *ngIf="unlockOptions.pin.enabled">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
block
|
||||
(click)="activeUnlockOption = UnlockOption.Pin"
|
||||
>
|
||||
{{ "unlockWithPin" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="unlockOptions.masterPassword.enabled">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
block
|
||||
(click)="activeUnlockOption = UnlockOption.MasterPassword"
|
||||
>
|
||||
{{ "unlockWithMasterPassword" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<button type="button" bitButton block (click)="logOut()">
|
||||
{{ "logOut" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<!-- PIN Unlock -->
|
||||
<ng-container *ngIf="unlockOptions.pin.enabled && activeUnlockOption === UnlockOption.Pin">
|
||||
<form [bitSubmit]="submit" [formGroup]="formGroup">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "pin" | i18n }}</bit-label>
|
||||
<input
|
||||
type="password"
|
||||
formControlName="pin"
|
||||
bitInput
|
||||
appAutofocus
|
||||
name="pin"
|
||||
class="tw-font-mono"
|
||||
required
|
||||
appInputVerbatim
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton
|
||||
bitSuffix
|
||||
bitPasswordInputToggle
|
||||
[(toggled)]="showPassword"
|
||||
></button>
|
||||
</bit-form-field>
|
||||
|
||||
<div class="tw-flex tw-flex-col tw-space-y-3">
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary" block>
|
||||
{{ "unlock" | i18n }}
|
||||
</button>
|
||||
|
||||
<p class="tw-text-center">{{ "or" | i18n }}</p>
|
||||
|
||||
<ng-container *ngIf="unlockOptions.biometrics.enabled">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
block
|
||||
(click)="activeUnlockOption = UnlockOption.Biometrics"
|
||||
>
|
||||
<span> {{ biometricUnlockBtnText | i18n }}</span>
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="unlockOptions.masterPassword.enabled">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
block
|
||||
(click)="activeUnlockOption = UnlockOption.MasterPassword"
|
||||
>
|
||||
{{ "unlockWithMasterPassword" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<button type="button" bitButton bitFormButton block (click)="logOut()">
|
||||
{{ "logOut" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</ng-container>
|
||||
|
||||
<!-- MP Unlock -->
|
||||
<ng-container
|
||||
*ngIf="
|
||||
unlockOptions.masterPassword.enabled && activeUnlockOption === UnlockOption.MasterPassword
|
||||
"
|
||||
>
|
||||
<form [bitSubmit]="submit" [formGroup]="formGroup">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "masterPass" | i18n }}</bit-label>
|
||||
<input
|
||||
type="password"
|
||||
formControlName="masterPassword"
|
||||
bitInput
|
||||
appAutofocus
|
||||
name="masterPassword"
|
||||
class="tw-font-mono"
|
||||
required
|
||||
appInputVerbatim
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton
|
||||
bitSuffix
|
||||
bitPasswordInputToggle
|
||||
[(toggled)]="showPassword"
|
||||
></button>
|
||||
|
||||
<!-- [attr.aria-pressed]="showPassword" -->
|
||||
</bit-form-field>
|
||||
|
||||
<div class="tw-flex tw-flex-col tw-space-y-3">
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary" block>
|
||||
{{ "unlock" | i18n }}
|
||||
</button>
|
||||
|
||||
<p class="tw-text-center">{{ "or" | i18n }}</p>
|
||||
|
||||
<ng-container *ngIf="unlockOptions.biometrics.enabled">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
block
|
||||
(click)="activeUnlockOption = UnlockOption.Biometrics"
|
||||
>
|
||||
<span> {{ biometricUnlockBtnText | i18n }}</span>
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="unlockOptions.pin.enabled">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
block
|
||||
(click)="activeUnlockOption = UnlockOption.Pin"
|
||||
>
|
||||
{{ "unlockWithPin" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<button type="button" bitButton bitFormButton block (click)="logOut()">
|
||||
{{ "logOut" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
638
libs/auth/src/angular/lock/lock.component.ts
Normal file
638
libs/auth/src/angular/lock/lock.component.ts
Normal file
@@ -0,0 +1,638 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||
import { Router } from "@angular/router";
|
||||
import { BehaviorSubject, firstValueFrom, Subject, switchMap, take, takeUntil } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import {
|
||||
MasterPasswordVerification,
|
||||
MasterPasswordVerificationResponse,
|
||||
} from "@bitwarden/common/auth/types/verification";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { KeySuffixOptions } from "@bitwarden/common/platform/enums";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { UserKey } from "@bitwarden/common/types/key";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
DialogService,
|
||||
FormFieldModule,
|
||||
IconButtonModule,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import { BiometricStateService } from "@bitwarden/key-management";
|
||||
|
||||
import { PinServiceAbstraction } from "../../common/abstractions";
|
||||
import { AnonLayoutWrapperDataService } from "../anon-layout/anon-layout-wrapper-data.service";
|
||||
|
||||
import {
|
||||
UnlockOption,
|
||||
LockComponentService,
|
||||
UnlockOptions,
|
||||
UnlockOptionValue,
|
||||
} from "./lock-component.service";
|
||||
|
||||
const BroadcasterSubscriptionId = "LockComponent";
|
||||
|
||||
const clientTypeToSuccessRouteRecord: Partial<Record<ClientType, string>> = {
|
||||
[ClientType.Web]: "vault",
|
||||
[ClientType.Desktop]: "vault",
|
||||
[ClientType.Browser]: "/tabs/current",
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: "bit-lock",
|
||||
templateUrl: "lock.component.html",
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
JslibModule,
|
||||
ReactiveFormsModule,
|
||||
ButtonModule,
|
||||
FormFieldModule,
|
||||
AsyncActionsModule,
|
||||
IconButtonModule,
|
||||
],
|
||||
})
|
||||
export class LockV2Component implements OnInit, OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
activeAccount: { id: UserId | undefined } & AccountInfo;
|
||||
|
||||
clientType: ClientType;
|
||||
ClientType = ClientType;
|
||||
|
||||
unlockOptions: UnlockOptions = null;
|
||||
|
||||
UnlockOption = UnlockOption;
|
||||
|
||||
private _activeUnlockOptionBSubject: BehaviorSubject<UnlockOptionValue> =
|
||||
new BehaviorSubject<UnlockOptionValue>(null);
|
||||
|
||||
activeUnlockOption$ = this._activeUnlockOptionBSubject.asObservable();
|
||||
|
||||
set activeUnlockOption(value: UnlockOptionValue) {
|
||||
this._activeUnlockOptionBSubject.next(value);
|
||||
}
|
||||
|
||||
get activeUnlockOption(): UnlockOptionValue {
|
||||
return this._activeUnlockOptionBSubject.value;
|
||||
}
|
||||
|
||||
private invalidPinAttempts = 0;
|
||||
|
||||
biometricUnlockBtnText: string;
|
||||
|
||||
// masterPassword = "";
|
||||
showPassword = false;
|
||||
private enforcedMasterPasswordOptions: MasterPasswordPolicyOptions = undefined;
|
||||
|
||||
forcePasswordResetRoute = "update-temp-password";
|
||||
|
||||
formGroup: FormGroup;
|
||||
|
||||
// Desktop properties:
|
||||
private deferFocus: boolean = null;
|
||||
private biometricAsked = false;
|
||||
|
||||
// Browser extension properties:
|
||||
private isInitialLockScreen = (window as any).previousPopupUrl == null;
|
||||
|
||||
defaultUnlockOptionSetForUser = false;
|
||||
|
||||
unlockingViaBiometrics = false;
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private pinService: PinServiceAbstraction,
|
||||
private userVerificationService: UserVerificationService,
|
||||
private cryptoService: CryptoService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private router: Router,
|
||||
private dialogService: DialogService,
|
||||
private messagingService: MessagingService,
|
||||
private biometricStateService: BiometricStateService,
|
||||
private ngZone: NgZone,
|
||||
private i18nService: I18nService,
|
||||
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
private logService: LogService,
|
||||
private deviceTrustService: DeviceTrustServiceAbstraction,
|
||||
private syncService: SyncService,
|
||||
private policyService: InternalPolicyService,
|
||||
private passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||
private formBuilder: FormBuilder,
|
||||
private toastService: ToastService,
|
||||
|
||||
private lockComponentService: LockComponentService,
|
||||
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
|
||||
|
||||
// desktop deps
|
||||
private broadcasterService: BroadcasterService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.listenForActiveUnlockOptionChanges();
|
||||
|
||||
// Listen for active account changes
|
||||
this.listenForActiveAccountChanges();
|
||||
|
||||
// Identify client
|
||||
this.clientType = this.platformUtilsService.getClientType();
|
||||
|
||||
if (this.clientType === "desktop") {
|
||||
await this.desktopOnInit();
|
||||
}
|
||||
}
|
||||
|
||||
// Base component methods
|
||||
private listenForActiveUnlockOptionChanges() {
|
||||
this.activeUnlockOption$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((activeUnlockOption: UnlockOptionValue) => {
|
||||
if (activeUnlockOption === UnlockOption.Pin) {
|
||||
this.buildPinForm();
|
||||
} else if (activeUnlockOption === UnlockOption.MasterPassword) {
|
||||
this.buildMasterPasswordForm();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private buildMasterPasswordForm() {
|
||||
this.formGroup = this.formBuilder.group(
|
||||
{
|
||||
masterPassword: ["", [Validators.required]],
|
||||
},
|
||||
{ updateOn: "submit" },
|
||||
);
|
||||
}
|
||||
|
||||
private buildPinForm() {
|
||||
this.formGroup = this.formBuilder.group(
|
||||
{
|
||||
pin: ["", [Validators.required]],
|
||||
},
|
||||
{ updateOn: "submit" },
|
||||
);
|
||||
}
|
||||
|
||||
private listenForActiveAccountChanges() {
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
switchMap((account) => {
|
||||
return this.handleActiveAccountChange(account);
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
private async handleActiveAccountChange(activeAccount: { id: UserId | undefined } & AccountInfo) {
|
||||
this.activeAccount = activeAccount;
|
||||
|
||||
this.resetDataOnActiveAccountChange();
|
||||
|
||||
this.setEmailAsPageSubtitle(activeAccount.email);
|
||||
|
||||
this.unlockOptions = await firstValueFrom(
|
||||
this.lockComponentService.getAvailableUnlockOptions$(activeAccount.id),
|
||||
);
|
||||
|
||||
this.setDefaultActiveUnlockOption(this.unlockOptions);
|
||||
|
||||
if (this.unlockOptions.biometrics.enabled) {
|
||||
await this.handleBiometricsUnlockEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
private resetDataOnActiveAccountChange() {
|
||||
this.defaultUnlockOptionSetForUser = false;
|
||||
this.unlockOptions = null;
|
||||
this.activeUnlockOption = null;
|
||||
this.formGroup = null; // new form group will be created based on new active unlock option
|
||||
|
||||
// Desktop properties:
|
||||
this.biometricAsked = false;
|
||||
}
|
||||
|
||||
private setEmailAsPageSubtitle(email: string) {
|
||||
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
|
||||
pageSubtitle: {
|
||||
subtitle: email,
|
||||
translate: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private setDefaultActiveUnlockOption(unlockOptions: UnlockOptions) {
|
||||
// Priorities should be Biometrics > Pin > Master Password for speed
|
||||
if (unlockOptions.biometrics.enabled) {
|
||||
this.activeUnlockOption = UnlockOption.Biometrics;
|
||||
} else if (unlockOptions.pin.enabled) {
|
||||
this.activeUnlockOption = UnlockOption.Pin;
|
||||
} else if (unlockOptions.masterPassword.enabled) {
|
||||
this.activeUnlockOption = UnlockOption.MasterPassword;
|
||||
}
|
||||
}
|
||||
|
||||
private async handleBiometricsUnlockEnabled() {
|
||||
this.biometricUnlockBtnText = this.lockComponentService.getBiometricsUnlockBtnText();
|
||||
|
||||
const autoPromptBiometrics = await firstValueFrom(
|
||||
this.biometricStateService.promptAutomatically$,
|
||||
);
|
||||
|
||||
// TODO: PM-12546 - we need to make our biometric autoprompt experience consistent between the
|
||||
// desktop and extension.
|
||||
if (this.clientType === "desktop") {
|
||||
if (autoPromptBiometrics) {
|
||||
await this.desktopAutoPromptBiometrics();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.clientType === "browser") {
|
||||
if (
|
||||
this.unlockOptions.biometrics.enabled &&
|
||||
autoPromptBiometrics &&
|
||||
this.isInitialLockScreen // only autoprompt biometrics on initial lock screen
|
||||
) {
|
||||
await this.unlockViaBiometrics();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Note: this submit method is only used for unlock methods that require a form and user input.
|
||||
// For biometrics unlock, the method is called directly.
|
||||
submit = async (): Promise<void> => {
|
||||
if (this.activeUnlockOption === UnlockOption.Pin) {
|
||||
return await this.unlockViaPin();
|
||||
}
|
||||
|
||||
await this.unlockViaMasterPassword();
|
||||
};
|
||||
|
||||
async logOut() {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "logOut" },
|
||||
content: { key: "logOutConfirmation" },
|
||||
acceptButtonText: { key: "logOut" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
this.messagingService.send("logout", { userId: this.activeAccount.id });
|
||||
}
|
||||
}
|
||||
|
||||
async unlockViaBiometrics(): Promise<void> {
|
||||
this.unlockingViaBiometrics = true;
|
||||
|
||||
if (!this.unlockOptions.biometrics.enabled) {
|
||||
this.unlockingViaBiometrics = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.biometricStateService.setUserPromptCancelled();
|
||||
const userKey = await this.cryptoService.getUserKeyFromStorage(
|
||||
KeySuffixOptions.Biometric,
|
||||
this.activeAccount.id,
|
||||
);
|
||||
|
||||
// If user cancels biometric prompt, userKey is undefined.
|
||||
if (userKey) {
|
||||
await this.setUserKeyAndContinue(userKey, false);
|
||||
}
|
||||
|
||||
this.unlockingViaBiometrics = false;
|
||||
} catch (e) {
|
||||
// Cancelling is a valid action.
|
||||
if (e?.message === "canceled") {
|
||||
this.unlockingViaBiometrics = false;
|
||||
return;
|
||||
}
|
||||
|
||||
let biometricTranslatedErrorDesc;
|
||||
|
||||
if (this.clientType === "browser") {
|
||||
const biometricErrorDescTranslationKey = this.lockComponentService.getBiometricsError(e);
|
||||
|
||||
if (biometricErrorDescTranslationKey) {
|
||||
biometricTranslatedErrorDesc = this.i18nService.t(biometricErrorDescTranslationKey);
|
||||
}
|
||||
}
|
||||
|
||||
// if no translation key found, show generic error message
|
||||
if (!biometricTranslatedErrorDesc) {
|
||||
biometricTranslatedErrorDesc = this.i18nService.t("unexpectedError");
|
||||
}
|
||||
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "error" },
|
||||
content: biometricTranslatedErrorDesc,
|
||||
acceptButtonText: { key: "tryAgain" },
|
||||
type: "danger",
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
// try again
|
||||
await this.unlockViaBiometrics();
|
||||
}
|
||||
|
||||
this.unlockingViaBiometrics = false;
|
||||
}
|
||||
}
|
||||
|
||||
togglePassword() {
|
||||
this.showPassword = !this.showPassword;
|
||||
const input = document.getElementById(
|
||||
this.unlockOptions.pin.enabled ? "pin" : "masterPassword",
|
||||
);
|
||||
if (this.ngZone.isStable) {
|
||||
input.focus();
|
||||
} else {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
this.ngZone.onStable.pipe(take(1)).subscribe(() => input.focus());
|
||||
}
|
||||
}
|
||||
|
||||
private validatePin(): boolean {
|
||||
if (this.formGroup.invalid) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("pinRequired"),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async unlockViaPin() {
|
||||
if (!this.validatePin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pin = this.formGroup.controls.pin.value;
|
||||
|
||||
const MAX_INVALID_PIN_ENTRY_ATTEMPTS = 5;
|
||||
|
||||
try {
|
||||
const userKey = await this.pinService.decryptUserKeyWithPin(pin, this.activeAccount.id);
|
||||
|
||||
if (userKey) {
|
||||
await this.setUserKeyAndContinue(userKey);
|
||||
return; // successfully unlocked
|
||||
}
|
||||
|
||||
// Failure state: invalid PIN or failed decryption
|
||||
this.invalidPinAttempts++;
|
||||
|
||||
// Log user out if they have entered an invalid PIN too many times
|
||||
if (this.invalidPinAttempts >= MAX_INVALID_PIN_ENTRY_ATTEMPTS) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("tooManyInvalidPinEntryAttemptsLoggingOut"),
|
||||
});
|
||||
this.messagingService.send("logout");
|
||||
return;
|
||||
}
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("invalidPin"),
|
||||
});
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("unexpectedError"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private validateMasterPassword(): boolean {
|
||||
if (this.formGroup.invalid) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("masterPasswordRequired"),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async unlockViaMasterPassword() {
|
||||
if (!this.validateMasterPassword()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const masterPassword = this.formGroup.controls.masterPassword.value;
|
||||
|
||||
const verification = {
|
||||
type: VerificationType.MasterPassword,
|
||||
secret: masterPassword,
|
||||
} as MasterPasswordVerification;
|
||||
|
||||
let passwordValid = false;
|
||||
let masterPasswordVerificationResponse: MasterPasswordVerificationResponse;
|
||||
try {
|
||||
masterPasswordVerificationResponse =
|
||||
await this.userVerificationService.verifyUserByMasterPassword(
|
||||
verification,
|
||||
this.activeAccount.id,
|
||||
this.activeAccount.email,
|
||||
);
|
||||
|
||||
this.enforcedMasterPasswordOptions = MasterPasswordPolicyOptions.fromResponse(
|
||||
masterPasswordVerificationResponse.policyOptions,
|
||||
);
|
||||
passwordValid = true;
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
if (!passwordValid) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("invalidMasterPassword"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
|
||||
masterPasswordVerificationResponse.masterKey,
|
||||
);
|
||||
await this.setUserKeyAndContinue(userKey, true);
|
||||
}
|
||||
|
||||
private async setUserKeyAndContinue(key: UserKey, evaluatePasswordAfterUnlock = false) {
|
||||
await this.cryptoService.setUserKey(key, this.activeAccount.id);
|
||||
|
||||
// Now that we have a decrypted user key in memory, we can check if we
|
||||
// need to establish trust on the current device
|
||||
await this.deviceTrustService.trustDeviceIfRequired(this.activeAccount.id);
|
||||
|
||||
await this.doContinue(evaluatePasswordAfterUnlock);
|
||||
}
|
||||
|
||||
private async doContinue(evaluatePasswordAfterUnlock: boolean) {
|
||||
await this.biometricStateService.resetUserPromptCancelled();
|
||||
this.messagingService.send("unlocked");
|
||||
|
||||
if (evaluatePasswordAfterUnlock) {
|
||||
try {
|
||||
// If we do not have any saved policies, attempt to load them from the service
|
||||
if (this.enforcedMasterPasswordOptions == undefined) {
|
||||
this.enforcedMasterPasswordOptions = await firstValueFrom(
|
||||
this.policyService.masterPasswordPolicyOptions$(),
|
||||
);
|
||||
}
|
||||
|
||||
if (this.requirePasswordChange()) {
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
await this.masterPasswordService.setForceSetPasswordReason(
|
||||
ForceSetPasswordReason.WeakMasterPassword,
|
||||
userId,
|
||||
);
|
||||
await this.router.navigate([this.forcePasswordResetRoute]);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
// Do not prevent unlock if there is an error evaluating policies
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Vault can be de-synced since notifications get ignored while locked. Need to check whether sync is required using the sync service.
|
||||
await this.syncService.fullSync(false);
|
||||
|
||||
if (this.clientType === "browser") {
|
||||
const previousUrl = this.lockComponentService.getPreviousUrl();
|
||||
if (previousUrl) {
|
||||
await this.router.navigateByUrl(previousUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// determine success route based on client type
|
||||
const successRoute = clientTypeToSuccessRouteRecord[this.clientType];
|
||||
await this.router.navigate([successRoute]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the master password meets the enforced policy requirements
|
||||
* If not, returns false
|
||||
*/
|
||||
private requirePasswordChange(): boolean {
|
||||
if (
|
||||
this.enforcedMasterPasswordOptions == undefined ||
|
||||
!this.enforcedMasterPasswordOptions.enforceOnLogin
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const masterPassword = this.formGroup.controls.masterPassword.value;
|
||||
|
||||
const passwordStrength = this.passwordStrengthService.getPasswordStrength(
|
||||
masterPassword,
|
||||
this.activeAccount.email,
|
||||
)?.score;
|
||||
|
||||
return !this.policyService.evaluateMasterPassword(
|
||||
passwordStrength,
|
||||
masterPassword,
|
||||
this.enforcedMasterPasswordOptions,
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// Desktop methods:
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
|
||||
async desktopOnInit() {
|
||||
// TODO: move this into a WindowService and subscribe to messages via MessageListener service.
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
||||
this.ngZone.run(() => {
|
||||
switch (message.command) {
|
||||
case "windowHidden":
|
||||
this.onWindowHidden();
|
||||
break;
|
||||
case "windowIsFocused":
|
||||
if (this.deferFocus === null) {
|
||||
this.deferFocus = !message.windowIsFocused;
|
||||
if (!this.deferFocus) {
|
||||
this.focusInput();
|
||||
}
|
||||
} else if (this.deferFocus && message.windowIsFocused) {
|
||||
this.focusInput();
|
||||
this.deferFocus = false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
});
|
||||
this.messagingService.send("getWindowIsFocused");
|
||||
}
|
||||
|
||||
private async desktopAutoPromptBiometrics() {
|
||||
if (!this.unlockOptions?.biometrics?.enabled || this.biometricAsked) {
|
||||
return;
|
||||
}
|
||||
|
||||
// prevent the biometric prompt from showing if the user has already cancelled it
|
||||
if (await firstValueFrom(this.biometricStateService.promptCancelled$)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const windowVisible = await this.lockComponentService.isWindowVisible();
|
||||
|
||||
if (windowVisible) {
|
||||
this.biometricAsked = true;
|
||||
await this.unlockViaBiometrics();
|
||||
}
|
||||
}
|
||||
|
||||
onWindowHidden() {
|
||||
this.showPassword = false;
|
||||
}
|
||||
|
||||
private focusInput() {
|
||||
if (this.unlockOptions) {
|
||||
document.getElementById(this.unlockOptions.pin.enabled ? "pin" : "masterPassword")?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
|
||||
if (this.clientType === "desktop") {
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ export enum FeatureFlag {
|
||||
GeneratorToolsModernization = "generator-tools-modernization",
|
||||
EnableConsolidatedBilling = "enable-consolidated-billing",
|
||||
AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section",
|
||||
EnableDeleteProvider = "AC-1218-delete-provider",
|
||||
ExtensionRefresh = "extension-refresh",
|
||||
PersistPopupView = "persist-popup-view",
|
||||
PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service",
|
||||
@@ -54,7 +53,6 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.GeneratorToolsModernization]: FALSE,
|
||||
[FeatureFlag.EnableConsolidatedBilling]: FALSE,
|
||||
[FeatureFlag.AC1795_UpdatedSubscriptionStatusSection]: FALSE,
|
||||
[FeatureFlag.EnableDeleteProvider]: FALSE,
|
||||
[FeatureFlag.ExtensionRefresh]: FALSE,
|
||||
[FeatureFlag.PersistPopupView]: FALSE,
|
||||
[FeatureFlag.PM4154_BulkEncryptionService]: FALSE,
|
||||
|
||||
@@ -36,5 +36,5 @@ export abstract class SendApiService {
|
||||
renewSendFileUploadUrl: (sendId: string, fileId: string) => Promise<SendFileUploadDataResponse>;
|
||||
removePassword: (id: string) => Promise<any>;
|
||||
delete: (id: string) => Promise<any>;
|
||||
save: (sendData: [Send, EncArrayBuffer]) => Promise<any>;
|
||||
save: (sendData: [Send, EncArrayBuffer]) => Promise<Send>;
|
||||
}
|
||||
|
||||
@@ -135,11 +135,12 @@ export class SendApiService implements SendApiServiceAbstraction {
|
||||
return this.apiService.send("DELETE", "/sends/" + id, null, true, false);
|
||||
}
|
||||
|
||||
async save(sendData: [Send, EncArrayBuffer]): Promise<any> {
|
||||
async save(sendData: [Send, EncArrayBuffer]): Promise<Send> {
|
||||
const response = await this.upload(sendData);
|
||||
|
||||
const data = new SendData(response);
|
||||
await this.sendService.upsert(data);
|
||||
return new Send(data);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<any> {
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
bitIconButton="bwi-clone"
|
||||
bitSuffix
|
||||
[appA11yTitle]="'copyPassword' | i18n"
|
||||
[disabled]="!sendOptionsForm.get('password').value"
|
||||
[valueLabel]="'password' | i18n"
|
||||
[appCopyClick]="sendOptionsForm.get('password').value"
|
||||
showToast
|
||||
|
||||
@@ -85,9 +85,14 @@ export class SendFormComponent implements AfterViewInit, OnInit, OnChanges, Send
|
||||
submitBtn?: ButtonComponent;
|
||||
|
||||
/**
|
||||
* Event emitted when the send is saved successfully.
|
||||
* Event emitted when the send is created successfully.
|
||||
*/
|
||||
@Output() sendSaved = new EventEmitter<SendView>();
|
||||
@Output() onSendCreated = new EventEmitter<SendView>();
|
||||
|
||||
/**
|
||||
* Event emitted when the send is updated successfully.
|
||||
*/
|
||||
@Output() onSendUpdated = new EventEmitter<SendView>();
|
||||
|
||||
/**
|
||||
* The original send being edited or cloned. Null for add mode.
|
||||
@@ -200,22 +205,26 @@ export class SendFormComponent implements AfterViewInit, OnInit, OnChanges, Send
|
||||
return;
|
||||
}
|
||||
|
||||
const sendView = await this.addEditFormService.saveSend(
|
||||
this.updatedSendView,
|
||||
this.file,
|
||||
this.config,
|
||||
);
|
||||
|
||||
if (this.config.mode === "add") {
|
||||
this.onSendCreated.emit(sendView);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Utils.isNullOrWhitespace(this.updatedSendView.password)) {
|
||||
this.updatedSendView.password = null;
|
||||
}
|
||||
|
||||
await this.addEditFormService.saveSend(this.updatedSendView, this.file, this.config);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t(
|
||||
this.config.mode === "edit" || this.config.mode === "partial-edit"
|
||||
? "editedItem"
|
||||
: "addedItem",
|
||||
),
|
||||
message: this.i18nService.t("editedItem"),
|
||||
});
|
||||
|
||||
this.sendSaved.emit(this.updatedSendView);
|
||||
this.onSendUpdated.emit(this.updatedSendView);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export class DefaultSendFormService implements SendFormService {
|
||||
|
||||
async saveSend(send: SendView, file: File | ArrayBuffer, config: SendFormConfig) {
|
||||
const sendData = await this.sendService.encrypt(send, file, send.password, null);
|
||||
return await this.sendApiService.save(sendData);
|
||||
const newSend = await this.sendApiService.save(sendData);
|
||||
return await this.decryptSend(newSend);
|
||||
}
|
||||
}
|
||||
|
||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -97,7 +97,7 @@
|
||||
"@storybook/theming": "8.2.9",
|
||||
"@types/argon2-browser": "1.18.4",
|
||||
"@types/chrome": "0.0.272",
|
||||
"@types/firefox-webext-browser": "111.0.5",
|
||||
"@types/firefox-webext-browser": "120.0.4",
|
||||
"@types/inquirer": "8.2.10",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/jquery": "3.5.30",
|
||||
@@ -9127,9 +9127,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/firefox-webext-browser": {
|
||||
"version": "111.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/firefox-webext-browser/-/firefox-webext-browser-111.0.5.tgz",
|
||||
"integrity": "sha512-YYE+4MeJvq7DZ+UzPD8c5uN1HJpGu4Fl6O6PEAfBJQmLzQkfTWlgMjZMJQHAmcH3rjVS5fjN+jMkkZ4ZTlKbmA==",
|
||||
"version": "120.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/firefox-webext-browser/-/firefox-webext-browser-120.0.4.tgz",
|
||||
"integrity": "sha512-lBrpf08xhiZBigrtdQfUaqX1UauwZ+skbFiL8u2Tdra/rklkKadYmIzTwkNZSWtuZ7OKpFqbE2HHfDoFqvZf6w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
"@storybook/theming": "8.2.9",
|
||||
"@types/argon2-browser": "1.18.4",
|
||||
"@types/chrome": "0.0.272",
|
||||
"@types/firefox-webext-browser": "111.0.5",
|
||||
"@types/firefox-webext-browser": "120.0.4",
|
||||
"@types/inquirer": "8.2.10",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/jquery": "3.5.30",
|
||||
|
||||
Reference in New Issue
Block a user