mirror of
https://github.com/bitwarden/browser
synced 2026-02-20 03:13:55 +00:00
merge main, fix conflicts
This commit is contained in:
@@ -3,9 +3,7 @@ import { DeviceManagementComponentServiceAbstraction } from "./device-management
|
||||
/**
|
||||
* Default implementation of the device management component service
|
||||
*/
|
||||
export class DefaultDeviceManagementComponentService
|
||||
implements DeviceManagementComponentServiceAbstraction
|
||||
{
|
||||
export class DefaultDeviceManagementComponentService implements DeviceManagementComponentServiceAbstraction {
|
||||
/**
|
||||
* Show header information in web client
|
||||
*/
|
||||
|
||||
@@ -5,11 +5,7 @@ import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { EmptyComponent } from "@bitwarden/angular/platform/guard/feature-flag.guard.spec";
|
||||
import {
|
||||
Account,
|
||||
AccountInfo,
|
||||
AccountService,
|
||||
} from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
@@ -18,6 +14,7 @@ import { KeyConnectorService } from "@bitwarden/common/key-management/key-connec
|
||||
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { authGuard } from "./auth.guard";
|
||||
@@ -38,16 +35,13 @@ describe("AuthGuard", () => {
|
||||
const accountService: MockProxy<AccountService> = mock<AccountService>();
|
||||
const activeAccountSubject = new BehaviorSubject<Account | null>(null);
|
||||
accountService.activeAccount$ = activeAccountSubject;
|
||||
activeAccountSubject.next(
|
||||
Object.assign(
|
||||
{
|
||||
name: "Test User 1",
|
||||
email: "test@email.com",
|
||||
emailVerified: true,
|
||||
} as AccountInfo,
|
||||
{ id: "test-id" as UserId },
|
||||
),
|
||||
);
|
||||
activeAccountSubject.next({
|
||||
id: "test-id" as UserId,
|
||||
...mockAccountInfoWith({
|
||||
name: "Test User 1",
|
||||
email: "test@email.com",
|
||||
}),
|
||||
});
|
||||
|
||||
if (featureFlag) {
|
||||
configService.getFeatureFlag.mockResolvedValue(true);
|
||||
|
||||
@@ -5,11 +5,7 @@ import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { EmptyComponent } from "@bitwarden/angular/platform/guard/feature-flag.guard.spec";
|
||||
import {
|
||||
Account,
|
||||
AccountInfo,
|
||||
AccountService,
|
||||
} from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
@@ -20,6 +16,7 @@ import { KeyConnectorDomainConfirmation } from "@bitwarden/common/key-management
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@@ -68,16 +65,13 @@ describe("lockGuard", () => {
|
||||
const accountService: MockProxy<AccountService> = mock<AccountService>();
|
||||
const activeAccountSubject = new BehaviorSubject<Account | null>(null);
|
||||
accountService.activeAccount$ = activeAccountSubject;
|
||||
activeAccountSubject.next(
|
||||
Object.assign(
|
||||
{
|
||||
name: "Test User 1",
|
||||
email: "test@email.com",
|
||||
emailVerified: true,
|
||||
} as AccountInfo,
|
||||
{ id: "test-id" as UserId },
|
||||
),
|
||||
);
|
||||
activeAccountSubject.next({
|
||||
id: "test-id" as UserId,
|
||||
...mockAccountInfoWith({
|
||||
name: "Test User 1",
|
||||
email: "test@email.com",
|
||||
}),
|
||||
});
|
||||
|
||||
const testBed = TestBed.configureTestingModule({
|
||||
imports: [
|
||||
|
||||
@@ -7,6 +7,7 @@ import { EmptyComponent } from "@bitwarden/angular/platform/guard/feature-flag.g
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { redirectToVaultIfUnlockedGuard } from "./redirect-to-vault-if-unlocked.guard";
|
||||
@@ -14,9 +15,10 @@ import { redirectToVaultIfUnlockedGuard } from "./redirect-to-vault-if-unlocked.
|
||||
describe("redirectToVaultIfUnlockedGuard", () => {
|
||||
const activeUser: Account = {
|
||||
id: "userId" as UserId,
|
||||
email: "test@email.com",
|
||||
emailVerified: true,
|
||||
name: "Test User",
|
||||
...mockAccountInfoWith({
|
||||
email: "test@email.com",
|
||||
name: "Test User",
|
||||
}),
|
||||
};
|
||||
|
||||
const setup = (activeUser: Account | null, authStatus: AuthenticationStatus | null) => {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@@ -17,9 +18,10 @@ import { tdeDecryptionRequiredGuard } from "./tde-decryption-required.guard";
|
||||
describe("tdeDecryptionRequiredGuard", () => {
|
||||
const activeUser: Account = {
|
||||
id: "fake_user_id" as UserId,
|
||||
email: "test@email.com",
|
||||
emailVerified: true,
|
||||
name: "Test User",
|
||||
...mockAccountInfoWith({
|
||||
email: "test@email.com",
|
||||
name: "Test User",
|
||||
}),
|
||||
};
|
||||
|
||||
const setup = (
|
||||
|
||||
@@ -10,6 +10,7 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@@ -18,9 +19,10 @@ import { unauthGuardFn } from "./unauth.guard";
|
||||
describe("UnauthGuard", () => {
|
||||
const activeUser: Account = {
|
||||
id: "fake_user_id" as UserId,
|
||||
email: "test@email.com",
|
||||
emailVerified: true,
|
||||
name: "Test User",
|
||||
...mockAccountInfoWith({
|
||||
email: "test@email.com",
|
||||
name: "Test User",
|
||||
}),
|
||||
};
|
||||
|
||||
const setup = (
|
||||
|
||||
@@ -11,6 +11,7 @@ import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/d
|
||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { DialogRef, DIALOG_DATA, ToastService } from "@bitwarden/components";
|
||||
import { LogService } from "@bitwarden/logging";
|
||||
@@ -47,10 +48,11 @@ describe("LoginApprovalDialogComponent", () => {
|
||||
validationService = mock<ValidationService>();
|
||||
|
||||
accountService.activeAccount$ = of({
|
||||
email: testEmail,
|
||||
id: "test-user-id" as UserId,
|
||||
emailVerified: true,
|
||||
name: null,
|
||||
...mockAccountInfoWith({
|
||||
email: testEmail,
|
||||
name: null,
|
||||
}),
|
||||
});
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { Router, RouterModule } from "@angular/router";
|
||||
import { ActivatedRoute, Router, RouterModule } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
@@ -19,6 +19,7 @@ import { ClientType } from "@bitwarden/common/enums";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
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 { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import {
|
||||
@@ -49,6 +50,7 @@ export type State = "assert" | "assertFailed";
|
||||
})
|
||||
export class LoginViaWebAuthnComponent implements OnInit {
|
||||
protected currentState: State = "assert";
|
||||
private shouldAutoClosePopout = false;
|
||||
|
||||
protected readonly Icons = {
|
||||
TwoFactorAuthSecurityKeyIcon,
|
||||
@@ -70,6 +72,7 @@ export class LoginViaWebAuthnComponent implements OnInit {
|
||||
constructor(
|
||||
private webAuthnLoginService: WebAuthnLoginServiceAbstraction,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private logService: LogService,
|
||||
private validationService: ValidationService,
|
||||
private i18nService: I18nService,
|
||||
@@ -77,9 +80,14 @@ export class LoginViaWebAuthnComponent implements OnInit {
|
||||
private keyService: KeyService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
|
||||
private messagingService: MessagingService,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Check if we should auto-close the popout after successful authentication
|
||||
this.shouldAutoClosePopout =
|
||||
this.route.snapshot.queryParamMap.get("autoClosePopout") === "true";
|
||||
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.authenticate();
|
||||
@@ -120,7 +128,18 @@ export class LoginViaWebAuthnComponent implements OnInit {
|
||||
// Only run loginSuccessHandlerService if webAuthn is used for vault decryption.
|
||||
const userKey = await firstValueFrom(this.keyService.userKey$(authResult.userId));
|
||||
if (userKey) {
|
||||
await this.loginSuccessHandlerService.run(authResult.userId);
|
||||
await this.loginSuccessHandlerService.run(authResult.userId, null);
|
||||
}
|
||||
|
||||
// If autoClosePopout is enabled and we're in a browser extension,
|
||||
// re-open the regular popup and close this popout window
|
||||
if (
|
||||
this.shouldAutoClosePopout &&
|
||||
this.platformUtilsService.getClientType() === ClientType.Browser
|
||||
) {
|
||||
this.messagingService.send("openPopup");
|
||||
window.close();
|
||||
return;
|
||||
}
|
||||
|
||||
await this.router.navigate([this.successRoute]);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/ma
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management";
|
||||
@@ -26,9 +27,11 @@ describe("DefaultChangePasswordService", () => {
|
||||
|
||||
const user: Account = {
|
||||
id: userId,
|
||||
email: "email",
|
||||
emailVerified: false,
|
||||
name: "name",
|
||||
...mockAccountInfoWith({
|
||||
email: "email",
|
||||
name: "name",
|
||||
emailVerified: false,
|
||||
}),
|
||||
};
|
||||
|
||||
const passwordInputResult: PasswordInputResult = {
|
||||
|
||||
@@ -15,6 +15,7 @@ import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/ma
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
|
||||
import { UpdateTdeOffboardingPasswordRequest } from "@bitwarden/common/auth/models/request/update-tde-offboarding-password.request";
|
||||
import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
@@ -44,6 +45,7 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
|
||||
protected organizationApiService: OrganizationApiServiceAbstraction,
|
||||
protected organizationUserApiService: OrganizationUserApiService,
|
||||
protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
|
||||
protected accountCryptographicStateService: AccountCryptographicStateService,
|
||||
) {}
|
||||
|
||||
async setInitialPassword(
|
||||
@@ -162,6 +164,14 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
|
||||
throw new Error("encrypted private key not found. Could not set private key in state.");
|
||||
}
|
||||
await this.keyService.setPrivateKey(keyPair[1].encryptedString, userId);
|
||||
await this.accountCryptographicStateService.setAccountCryptographicState(
|
||||
{
|
||||
V1: {
|
||||
private_key: keyPair[1].encryptedString,
|
||||
},
|
||||
},
|
||||
userId,
|
||||
);
|
||||
}
|
||||
|
||||
await this.masterPasswordService.setMasterKeyHash(newLocalMasterKeyHash, userId);
|
||||
@@ -198,10 +208,13 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
|
||||
userId: UserId,
|
||||
) {
|
||||
const userDecryptionOpts = await firstValueFrom(
|
||||
this.userDecryptionOptionsService.userDecryptionOptions$,
|
||||
this.userDecryptionOptionsService.userDecryptionOptionsById$(userId),
|
||||
);
|
||||
userDecryptionOpts.hasMasterPassword = true;
|
||||
await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts);
|
||||
await this.userDecryptionOptionsService.setUserDecryptionOptionsById(
|
||||
userId,
|
||||
userDecryptionOpts,
|
||||
);
|
||||
await this.kdfConfigService.setKdfConfig(userId, kdfConfig);
|
||||
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
||||
await this.keyService.setUserKey(masterKeyEncryptedUserKey[0], userId);
|
||||
|
||||
@@ -20,6 +20,7 @@ import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/ma
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
|
||||
import { UpdateTdeOffboardingPasswordRequest } from "@bitwarden/common/auth/models/request/update-tde-offboarding-password.request";
|
||||
import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import {
|
||||
EncryptedString,
|
||||
@@ -56,6 +57,7 @@ describe("DefaultSetInitialPasswordService", () => {
|
||||
let organizationApiService: MockProxy<OrganizationApiServiceAbstraction>;
|
||||
let organizationUserApiService: MockProxy<OrganizationUserApiService>;
|
||||
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
|
||||
let accountCryptographicStateService: MockProxy<AccountCryptographicStateService>;
|
||||
|
||||
let userId: UserId;
|
||||
let userKey: UserKey;
|
||||
@@ -73,6 +75,7 @@ describe("DefaultSetInitialPasswordService", () => {
|
||||
organizationApiService = mock<OrganizationApiServiceAbstraction>();
|
||||
organizationUserApiService = mock<OrganizationUserApiService>();
|
||||
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
|
||||
accountCryptographicStateService = mock<AccountCryptographicStateService>();
|
||||
|
||||
userId = "userId" as UserId;
|
||||
userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
|
||||
@@ -90,6 +93,7 @@ describe("DefaultSetInitialPasswordService", () => {
|
||||
organizationApiService,
|
||||
organizationUserApiService,
|
||||
userDecryptionOptionsService,
|
||||
accountCryptographicStateService,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -149,7 +153,9 @@ describe("DefaultSetInitialPasswordService", () => {
|
||||
|
||||
userDecryptionOptions = new UserDecryptionOptions({ hasMasterPassword: true });
|
||||
userDecryptionOptionsSubject = new BehaviorSubject(userDecryptionOptions);
|
||||
userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject;
|
||||
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
|
||||
userDecryptionOptionsSubject,
|
||||
);
|
||||
|
||||
setPasswordRequest = new SetPasswordRequest(
|
||||
credentials.newServerMasterKeyHash,
|
||||
@@ -362,7 +368,8 @@ describe("DefaultSetInitialPasswordService", () => {
|
||||
|
||||
// Assert
|
||||
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||
expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith(
|
||||
expect(userDecryptionOptionsService.setUserDecryptionOptionsById).toHaveBeenCalledWith(
|
||||
userId,
|
||||
userDecryptionOptions,
|
||||
);
|
||||
expect(kdfConfigService.setKdfConfig).toHaveBeenCalledWith(userId, credentials.kdfConfig);
|
||||
@@ -383,6 +390,16 @@ describe("DefaultSetInitialPasswordService", () => {
|
||||
// Assert
|
||||
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||
expect(keyService.setPrivateKey).toHaveBeenCalledWith(keyPair[1].encryptedString, userId);
|
||||
expect(
|
||||
accountCryptographicStateService.setAccountCryptographicState,
|
||||
).toHaveBeenCalledWith(
|
||||
{
|
||||
V1: {
|
||||
private_key: keyPair[1].encryptedString as EncryptedString,
|
||||
},
|
||||
},
|
||||
userId,
|
||||
);
|
||||
});
|
||||
|
||||
it("should set the local master key hash to state", async () => {
|
||||
@@ -560,7 +577,8 @@ describe("DefaultSetInitialPasswordService", () => {
|
||||
|
||||
// Assert
|
||||
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||
expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith(
|
||||
expect(userDecryptionOptionsService.setUserDecryptionOptionsById).toHaveBeenCalledWith(
|
||||
userId,
|
||||
userDecryptionOptions,
|
||||
);
|
||||
expect(kdfConfigService.setKdfConfig).toHaveBeenCalledWith(userId, credentials.kdfConfig);
|
||||
|
||||
@@ -14,10 +14,11 @@ import { BadgeModule } from "@bitwarden/components";
|
||||
type="button"
|
||||
*appNotPremium
|
||||
bitBadge
|
||||
variant="success"
|
||||
[variant]="'primary'"
|
||||
class="!tw-text-primary-600 !tw-border-primary-600"
|
||||
(click)="promptForPremium($event)"
|
||||
>
|
||||
{{ "premium" | i18n }}
|
||||
<i class="bwi bwi-premium tw-pe-1"></i>{{ "upgrade" | i18n }}
|
||||
</button>
|
||||
`,
|
||||
imports: [BadgeModule, JslibModule],
|
||||
|
||||
@@ -29,7 +29,7 @@ export default {
|
||||
provide: I18nService,
|
||||
useFactory: () => {
|
||||
return new I18nMockService({
|
||||
premium: "Premium",
|
||||
upgrade: "Upgrade",
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -20,33 +20,35 @@
|
||||
<div
|
||||
class="tw-box-border tw-bg-background tw-text-main tw-size-full tw-flex tw-flex-col tw-px-8 tw-pb-2 tw-w-full tw-max-w-md"
|
||||
>
|
||||
<div class="tw-flex tw-items-center tw-justify-between tw-mb-2">
|
||||
<div class="tw-flex tw-items-center tw-justify-between">
|
||||
<h3 slot="title" class="tw-m-0" bitTypography="h3">
|
||||
{{ "upgradeToPremium" | i18n }}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<!-- Tagline with consistent height (exactly 2 lines) -->
|
||||
<div class="tw-mb-6 tw-h-6">
|
||||
<div class="tw-h-6">
|
||||
<p bitTypography="helper" class="tw-text-muted tw-m-0 tw-leading-relaxed tw-line-clamp-2">
|
||||
{{ cardDetails.tagline }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Price Section -->
|
||||
<div class="tw-mb-6">
|
||||
<div class="tw-flex tw-items-baseline tw-gap-1 tw-flex-wrap">
|
||||
<span class="tw-text-3xl tw-font-medium tw-leading-none tw-m-0">{{
|
||||
cardDetails.price.amount | currency: "$"
|
||||
}}</span>
|
||||
<span bitTypography="helper" class="tw-text-muted">
|
||||
/ {{ cardDetails.price.cadence }}
|
||||
</span>
|
||||
@if (cardDetails.price) {
|
||||
<div class="tw-mt-5">
|
||||
<div class="tw-flex tw-items-baseline tw-gap-1 tw-flex-wrap">
|
||||
<span class="tw-text-3xl tw-font-medium tw-leading-none tw-m-0">{{
|
||||
cardDetails.price.amount | currency: "$"
|
||||
}}</span>
|
||||
<span bitTypography="helper" class="tw-text-muted">
|
||||
/ {{ cardDetails.price.cadence | i18n }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Button space (always reserved) -->
|
||||
<div class="tw-mb-6 tw-h-12">
|
||||
<div class="tw-my-5 tw-h-12">
|
||||
<button
|
||||
bitButton
|
||||
[buttonType]="cardDetails.button.type"
|
||||
|
||||
@@ -40,6 +40,7 @@ describe("PremiumUpgradeDialogComponent", () => {
|
||||
type: "standalone",
|
||||
annualPrice: 10,
|
||||
annualPricePerAdditionalStorageGB: 4,
|
||||
providedStorageGB: 1,
|
||||
features: [
|
||||
{ key: "feature1", value: "Feature 1" },
|
||||
{ key: "feature2", value: "Feature 2" },
|
||||
@@ -58,6 +59,7 @@ describe("PremiumUpgradeDialogComponent", () => {
|
||||
users: 6,
|
||||
annualPrice: 40,
|
||||
annualPricePerAdditionalStorageGB: 4,
|
||||
providedStorageGB: 1,
|
||||
features: [{ key: "featureA", value: "Feature A" }],
|
||||
},
|
||||
};
|
||||
@@ -204,4 +206,39 @@ describe("PremiumUpgradeDialogComponent", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("self-hosted environment", () => {
|
||||
it("should handle null price data for self-hosted environment", async () => {
|
||||
const selfHostedPremiumTier: PersonalSubscriptionPricingTier = {
|
||||
id: PersonalSubscriptionPricingTierIds.Premium,
|
||||
name: "Premium",
|
||||
description: "Advanced features for power users",
|
||||
availableCadences: [SubscriptionCadenceIds.Annually],
|
||||
passwordManager: {
|
||||
type: "standalone",
|
||||
annualPrice: undefined as any, // self-host will have these prices empty
|
||||
annualPricePerAdditionalStorageGB: undefined as any,
|
||||
providedStorageGB: undefined as any,
|
||||
features: [
|
||||
{ key: "feature1", value: "Feature 1" },
|
||||
{ key: "feature2", value: "Feature 2" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
mockSubscriptionPricingService.getPersonalSubscriptionPricingTiers$.mockReturnValue(
|
||||
of([selfHostedPremiumTier]),
|
||||
);
|
||||
|
||||
const selfHostedFixture = TestBed.createComponent(PremiumUpgradeDialogComponent);
|
||||
const selfHostedComponent = selfHostedFixture.componentInstance;
|
||||
selfHostedFixture.detectChanges();
|
||||
|
||||
const cardDetails = await firstValueFrom(selfHostedComponent["cardDetails$"]);
|
||||
|
||||
expect(cardDetails?.title).toBe("Premium");
|
||||
expect(cardDetails?.price).toBeUndefined();
|
||||
expect(cardDetails?.features).toEqual(["Feature 1", "Feature 2"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,6 +31,24 @@ const mockPremiumTier: PersonalSubscriptionPricingTier = {
|
||||
type: "standalone",
|
||||
annualPrice: 10,
|
||||
annualPricePerAdditionalStorageGB: 4,
|
||||
providedStorageGB: 1,
|
||||
features: [
|
||||
{ key: "builtInAuthenticator", value: "Built-in authenticator" },
|
||||
{ key: "secureFileStorage", value: "Secure file storage" },
|
||||
{ key: "emergencyAccess", value: "Emergency access" },
|
||||
{ key: "breachMonitoring", value: "Breach monitoring" },
|
||||
{ key: "andMoreFeatures", value: "And more!" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const mockPremiumTierNoPricingData: PersonalSubscriptionPricingTier = {
|
||||
id: PersonalSubscriptionPricingTierIds.Premium,
|
||||
name: "Premium",
|
||||
description: "Complete online security",
|
||||
availableCadences: [SubscriptionCadenceIds.Annually],
|
||||
passwordManager: {
|
||||
type: "standalone",
|
||||
features: [
|
||||
{ key: "builtInAuthenticator", value: "Built-in authenticator" },
|
||||
{ key: "secureFileStorage", value: "Secure file storage" },
|
||||
@@ -85,11 +103,11 @@ export default {
|
||||
t: (key: string) => {
|
||||
switch (key) {
|
||||
case "upgradeNow":
|
||||
return "Upgrade Now";
|
||||
return "Upgrade now";
|
||||
case "month":
|
||||
return "month";
|
||||
case "upgradeToPremium":
|
||||
return "Upgrade To Premium";
|
||||
return "Upgrade to Premium";
|
||||
default:
|
||||
return key;
|
||||
}
|
||||
@@ -115,3 +133,18 @@ export default {
|
||||
|
||||
type Story = StoryObj<PremiumUpgradeDialogComponent>;
|
||||
export const Default: Story = {};
|
||||
|
||||
export const NoPricingData: Story = {
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
providers: [
|
||||
{
|
||||
provide: SubscriptionPricingServiceAbstraction,
|
||||
useValue: {
|
||||
getPersonalSubscriptionPricingTiers$: () => of([mockPremiumTierNoPricingData]),
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -3,12 +3,12 @@ import { CommonModule } from "@angular/common";
|
||||
import { ChangeDetectionStrategy, Component } from "@angular/core";
|
||||
import { catchError, EMPTY, firstValueFrom, map, Observable } from "rxjs";
|
||||
|
||||
import { SubscriptionPricingCardDetails } from "@bitwarden/angular/billing/types/subscription-pricing-card-details";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { SubscriptionPricingServiceAbstraction } from "@bitwarden/common/billing/abstractions/subscription-pricing.service.abstraction";
|
||||
import {
|
||||
PersonalSubscriptionPricingTier,
|
||||
PersonalSubscriptionPricingTierIds,
|
||||
SubscriptionCadence,
|
||||
SubscriptionCadenceIds,
|
||||
} from "@bitwarden/common/billing/types/subscription-pricing-tier";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
@@ -16,7 +16,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import {
|
||||
ButtonModule,
|
||||
ButtonType,
|
||||
CenterPositionStrategy,
|
||||
DialogModule,
|
||||
DialogRef,
|
||||
@@ -27,14 +26,6 @@ import {
|
||||
} from "@bitwarden/components";
|
||||
import { LogService } from "@bitwarden/logging";
|
||||
|
||||
type CardDetails = {
|
||||
title: string;
|
||||
tagline: string;
|
||||
price: { amount: number; cadence: SubscriptionCadence };
|
||||
button: { text: string; type: ButtonType; icon?: { type: string; position: "before" | "after" } };
|
||||
features: string[];
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: "billing-premium-upgrade-dialog",
|
||||
standalone: true,
|
||||
@@ -51,9 +42,8 @@ type CardDetails = {
|
||||
templateUrl: "./premium-upgrade-dialog.component.html",
|
||||
})
|
||||
export class PremiumUpgradeDialogComponent {
|
||||
protected cardDetails$: Observable<CardDetails | null> = this.subscriptionPricingService
|
||||
.getPersonalSubscriptionPricingTiers$()
|
||||
.pipe(
|
||||
protected cardDetails$: Observable<SubscriptionPricingCardDetails | null> =
|
||||
this.subscriptionPricingService.getPersonalSubscriptionPricingTiers$().pipe(
|
||||
map((tiers) => tiers.find((tier) => tier.id === PersonalSubscriptionPricingTierIds.Premium)),
|
||||
map((tier) => this.mapPremiumTierToCardDetails(tier!)),
|
||||
catchError((error: unknown) => {
|
||||
@@ -91,14 +81,18 @@ export class PremiumUpgradeDialogComponent {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
private mapPremiumTierToCardDetails(tier: PersonalSubscriptionPricingTier): CardDetails {
|
||||
private mapPremiumTierToCardDetails(
|
||||
tier: PersonalSubscriptionPricingTier,
|
||||
): SubscriptionPricingCardDetails {
|
||||
return {
|
||||
title: tier.name,
|
||||
tagline: tier.description,
|
||||
price: {
|
||||
amount: tier.passwordManager.annualPrice / 12,
|
||||
cadence: SubscriptionCadenceIds.Monthly,
|
||||
},
|
||||
price: tier.passwordManager.annualPrice
|
||||
? {
|
||||
amount: tier.passwordManager.annualPrice / 12,
|
||||
cadence: SubscriptionCadenceIds.Monthly,
|
||||
}
|
||||
: undefined,
|
||||
button: {
|
||||
text: this.i18nService.t("upgradeNow"),
|
||||
type: "primary",
|
||||
|
||||
@@ -5,6 +5,7 @@ import { firstValueFrom, Observable, switchMap } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -16,6 +17,7 @@ import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/com
|
||||
export class PremiumComponent implements OnInit {
|
||||
isPremium$: Observable<boolean>;
|
||||
price = 10;
|
||||
storageProvidedGb = 0;
|
||||
refreshPromise: Promise<any>;
|
||||
cloudWebVaultUrl: string;
|
||||
|
||||
@@ -29,6 +31,7 @@ export class PremiumComponent implements OnInit {
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private toastService: ToastService,
|
||||
accountService: AccountService,
|
||||
private billingApiService: BillingApiServiceAbstraction,
|
||||
) {
|
||||
this.isPremium$ = accountService.activeAccount$.pipe(
|
||||
switchMap((account) =>
|
||||
@@ -39,6 +42,9 @@ export class PremiumComponent implements OnInit {
|
||||
|
||||
async ngOnInit() {
|
||||
this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$);
|
||||
const premiumResponse = await this.billingApiService.getPremiumPlan();
|
||||
this.storageProvidedGb = premiumResponse.storage.provided;
|
||||
this.price = premiumResponse.seat.price;
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { SubscriptionCadence } from "@bitwarden/common/billing/types/subscription-pricing-tier";
|
||||
import { ButtonType } from "@bitwarden/components";
|
||||
|
||||
export type SubscriptionPricingCardDetails = {
|
||||
title: string;
|
||||
tagline: string;
|
||||
price?: { amount: number; cadence: SubscriptionCadence };
|
||||
button: { text: string; type: ButtonType; icon?: { type: string; position: "before" | "after" } };
|
||||
features: string[];
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
<bit-callout [icon]="icon" [title]="title" [type]="$any(type)" [useAlertRole]="useAlertRole">
|
||||
<div class="tw-pl-7 tw-m-0" *ngIf="enforcedPolicyOptions">
|
||||
{{ enforcedPolicyMessage }}
|
||||
<ul>
|
||||
<li *ngIf="enforcedPolicyOptions?.minComplexity > 0">
|
||||
{{ "policyInEffectMinComplexity" | i18n: getPasswordScoreAlertDisplay() }}
|
||||
</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.minLength > 0">
|
||||
{{ "policyInEffectMinLength" | i18n: enforcedPolicyOptions?.minLength.toString() }}
|
||||
</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireUpper">
|
||||
{{ "policyInEffectUppercase" | i18n }}
|
||||
</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireLower">
|
||||
{{ "policyInEffectLowercase" | i18n }}
|
||||
</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireNumbers">
|
||||
{{ "policyInEffectNumbers" | i18n }}
|
||||
</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireSpecial">
|
||||
{{ "policyInEffectSpecial" | i18n: "!@#$%^&*" }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<ng-content></ng-content>
|
||||
</bit-callout>
|
||||
@@ -1,70 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CalloutTypes } from "@bitwarden/components";
|
||||
|
||||
/**
|
||||
* @deprecated use the CL's `CalloutComponent` instead
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-callout",
|
||||
templateUrl: "callout.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class DeprecatedCalloutComponent implements OnInit {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() type: CalloutTypes = "info";
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() icon: string;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() title: string;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() enforcedPolicyMessage: string;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() useAlertRole = false;
|
||||
|
||||
calloutStyle: string;
|
||||
|
||||
constructor(private i18nService: I18nService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.calloutStyle = this.type;
|
||||
|
||||
if (this.enforcedPolicyMessage === undefined) {
|
||||
this.enforcedPolicyMessage = this.i18nService.t("masterPasswordPolicyInEffect");
|
||||
}
|
||||
}
|
||||
|
||||
getPasswordScoreAlertDisplay() {
|
||||
if (this.enforcedPolicyOptions == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
let str: string;
|
||||
switch (this.enforcedPolicyOptions.minComplexity) {
|
||||
case 4:
|
||||
str = this.i18nService.t("strong");
|
||||
break;
|
||||
case 3:
|
||||
str = this.i18nService.t("good");
|
||||
break;
|
||||
default:
|
||||
str = this.i18nService.t("weak");
|
||||
break;
|
||||
}
|
||||
return str + " (" + this.enforcedPolicyOptions.minComplexity + ")";
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { InjectFlags, InjectOptions, Injector, ProviderToken } from "@angular/core";
|
||||
import { InjectOptions, Injector, ProviderToken } from "@angular/core";
|
||||
|
||||
export class ModalInjector implements Injector {
|
||||
constructor(
|
||||
@@ -12,8 +12,8 @@ export class ModalInjector implements Injector {
|
||||
options: InjectOptions & { optional?: false },
|
||||
): T;
|
||||
get<T>(token: ProviderToken<T>, notFoundValue: null, options: InjectOptions): T;
|
||||
get<T>(token: ProviderToken<T>, notFoundValue?: T, options?: InjectOptions | InjectFlags): T;
|
||||
get<T>(token: ProviderToken<T>, notFoundValue?: T, flags?: InjectFlags): T;
|
||||
get<T>(token: ProviderToken<T>, notFoundValue?: T, options?: InjectOptions | null): T;
|
||||
get<T>(token: ProviderToken<T>, notFoundValue?: T, flags?: null): T;
|
||||
get(token: any, notFoundValue?: any): any;
|
||||
get(token: any, notFoundValue?: any, flags?: any): any {
|
||||
return this._additionalTokens.get(token) ?? this._parentInjector.get<any>(token, notFoundValue);
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
|
||||
import { TwoFactorIconComponent } from "./auth/components/two-factor-icon.component";
|
||||
import { NotPremiumDirective } from "./billing/directives/not-premium.directive";
|
||||
import { DeprecatedCalloutComponent } from "./components/callout.component";
|
||||
import { A11yInvalidDirective } from "./directives/a11y-invalid.directive";
|
||||
import { ApiActionDirective } from "./directives/api-action.directive";
|
||||
import { BoxRowDirective } from "./directives/box-row.directive";
|
||||
@@ -86,7 +85,6 @@ import { IconComponent } from "./vault/components/icon.component";
|
||||
A11yInvalidDirective,
|
||||
ApiActionDirective,
|
||||
BoxRowDirective,
|
||||
DeprecatedCalloutComponent,
|
||||
CopyTextDirective,
|
||||
CreditCardNumberPipe,
|
||||
EllipsisPipe,
|
||||
@@ -115,7 +113,6 @@ import { IconComponent } from "./vault/components/icon.component";
|
||||
AutofocusDirective,
|
||||
ToastModule,
|
||||
BoxRowDirective,
|
||||
DeprecatedCalloutComponent,
|
||||
CopyTextDirective,
|
||||
CreditCardNumberPipe,
|
||||
EllipsisPipe,
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
export abstract class EncryptedMigrationsSchedulerService {
|
||||
/**
|
||||
* Runs migrations for a user if needed, handling both interactive and non-interactive cases
|
||||
* @param userId The user ID to run migrations for
|
||||
*/
|
||||
abstract runMigrationsIfNeeded(userId: UserId): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
import { Router } from "@angular/router";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { EncryptedMigrator } from "@bitwarden/common/key-management/encrypted-migrator/encrypted-migrator.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SingleUserState, StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { mockAccountInfoWith, FakeAccountService } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { LogService } from "@bitwarden/logging";
|
||||
|
||||
import {
|
||||
DefaultEncryptedMigrationsSchedulerService,
|
||||
ENCRYPTED_MIGRATION_DISMISSED,
|
||||
} from "./encrypted-migrations-scheduler.service";
|
||||
import { PromptMigrationPasswordComponent } from "./prompt-migration-password.component";
|
||||
|
||||
const SomeUser = "SomeUser" as UserId;
|
||||
const AnotherUser = "SomeOtherUser" as UserId;
|
||||
const accounts = {
|
||||
[SomeUser]: mockAccountInfoWith({
|
||||
name: "some user",
|
||||
email: "some.user@example.com",
|
||||
}),
|
||||
[AnotherUser]: mockAccountInfoWith({
|
||||
name: "some other user",
|
||||
email: "some.other.user@example.com",
|
||||
}),
|
||||
};
|
||||
|
||||
describe("DefaultEncryptedMigrationsSchedulerService", () => {
|
||||
let service: DefaultEncryptedMigrationsSchedulerService;
|
||||
const mockAccountService = new FakeAccountService(accounts);
|
||||
const mockAuthService = mock<AuthService>();
|
||||
const mockEncryptedMigrator = mock<EncryptedMigrator>();
|
||||
const mockStateProvider = mock<StateProvider>();
|
||||
const mockSyncService = mock<SyncService>();
|
||||
const mockDialogService = mock<DialogService>();
|
||||
const mockToastService = mock<ToastService>();
|
||||
const mockI18nService = mock<I18nService>();
|
||||
const mockLogService = mock<LogService>();
|
||||
const mockRouter = mock<Router>();
|
||||
|
||||
const mockUserId = "test-user-id" as UserId;
|
||||
const mockMasterPassword = "test-master-password";
|
||||
|
||||
const createMockUserState = <T>(value: T): jest.Mocked<SingleUserState<T>> =>
|
||||
({
|
||||
state$: of(value),
|
||||
userId: mockUserId,
|
||||
update: jest.fn(),
|
||||
combinedState$: of([mockUserId, value]),
|
||||
}) as any;
|
||||
|
||||
beforeEach(() => {
|
||||
const mockDialogRef = {
|
||||
closed: of(mockMasterPassword),
|
||||
};
|
||||
|
||||
jest.spyOn(PromptMigrationPasswordComponent, "open").mockReturnValue(mockDialogRef as any);
|
||||
mockI18nService.t.mockReturnValue("translated_migrationsFailed");
|
||||
(mockRouter as any)["events"] = of({ url: "/vault" }) as any;
|
||||
|
||||
service = new DefaultEncryptedMigrationsSchedulerService(
|
||||
mockSyncService,
|
||||
mockAccountService,
|
||||
mockStateProvider,
|
||||
mockEncryptedMigrator,
|
||||
mockAuthService,
|
||||
mockLogService,
|
||||
mockDialogService,
|
||||
mockToastService,
|
||||
mockI18nService,
|
||||
mockRouter,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("runMigrationsIfNeeded", () => {
|
||||
it("should return early if user is not unlocked", async () => {
|
||||
mockAuthService.authStatusFor$.mockReturnValue(of(AuthenticationStatus.Locked));
|
||||
|
||||
await service.runMigrationsIfNeeded(mockUserId);
|
||||
|
||||
expect(mockEncryptedMigrator.needsMigrations).not.toHaveBeenCalled();
|
||||
expect(mockLogService.info).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should log and return when no migration is needed", async () => {
|
||||
mockAuthService.authStatusFor$.mockReturnValue(of(AuthenticationStatus.Unlocked));
|
||||
mockEncryptedMigrator.needsMigrations.mockResolvedValue("noMigrationNeeded");
|
||||
|
||||
await service.runMigrationsIfNeeded(mockUserId);
|
||||
|
||||
expect(mockEncryptedMigrator.needsMigrations).toHaveBeenCalledWith(mockUserId);
|
||||
expect(mockLogService.info).toHaveBeenCalledWith(
|
||||
`[EncryptedMigrationsScheduler] No migrations needed for user ${mockUserId}`,
|
||||
);
|
||||
expect(mockEncryptedMigrator.runMigrations).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should run migrations without interaction when master password is not required", async () => {
|
||||
mockAuthService.authStatusFor$.mockReturnValue(of(AuthenticationStatus.Unlocked));
|
||||
mockEncryptedMigrator.needsMigrations.mockResolvedValue("needsMigration");
|
||||
|
||||
await service.runMigrationsIfNeeded(mockUserId);
|
||||
|
||||
expect(mockEncryptedMigrator.needsMigrations).toHaveBeenCalledWith(mockUserId);
|
||||
expect(mockLogService.info).toHaveBeenCalledWith(
|
||||
`[EncryptedMigrationsScheduler] User ${mockUserId} needs migrations with master password`,
|
||||
);
|
||||
expect(mockEncryptedMigrator.runMigrations).toHaveBeenCalledWith(mockUserId, null);
|
||||
});
|
||||
|
||||
it("should run migrations with interaction when migration is needed", async () => {
|
||||
mockAuthService.authStatusFor$.mockReturnValue(of(AuthenticationStatus.Unlocked));
|
||||
mockEncryptedMigrator.needsMigrations.mockResolvedValue("needsMigrationWithMasterPassword");
|
||||
const mockUserState = createMockUserState(null);
|
||||
mockStateProvider.getUser.mockReturnValue(mockUserState);
|
||||
|
||||
await service.runMigrationsIfNeeded(mockUserId);
|
||||
|
||||
expect(mockEncryptedMigrator.needsMigrations).toHaveBeenCalledWith(mockUserId);
|
||||
expect(mockLogService.info).toHaveBeenCalledWith(
|
||||
`[EncryptedMigrationsScheduler] User ${mockUserId} needs migrations with master password`,
|
||||
);
|
||||
expect(PromptMigrationPasswordComponent.open).toHaveBeenCalledWith(mockDialogService);
|
||||
expect(mockEncryptedMigrator.runMigrations).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
mockMasterPassword,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("runMigrationsWithoutInteraction", () => {
|
||||
it("should run migrations without master password", async () => {
|
||||
mockAuthService.authStatusFor$.mockReturnValue(of(AuthenticationStatus.Unlocked));
|
||||
mockEncryptedMigrator.needsMigrations.mockResolvedValue("needsMigration");
|
||||
|
||||
await service.runMigrationsIfNeeded(mockUserId);
|
||||
|
||||
expect(mockEncryptedMigrator.runMigrations).toHaveBeenCalledWith(mockUserId, null);
|
||||
expect(mockLogService.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle errors during migration without interaction", async () => {
|
||||
const mockError = new Error("Migration failed");
|
||||
mockAuthService.authStatusFor$.mockReturnValue(of(AuthenticationStatus.Unlocked));
|
||||
mockEncryptedMigrator.needsMigrations.mockResolvedValue("needsMigration");
|
||||
mockEncryptedMigrator.runMigrations.mockRejectedValue(mockError);
|
||||
|
||||
await service.runMigrationsIfNeeded(mockUserId);
|
||||
|
||||
expect(mockEncryptedMigrator.runMigrations).toHaveBeenCalledWith(mockUserId, null);
|
||||
expect(mockLogService.error).toHaveBeenCalledWith(
|
||||
"[EncryptedMigrationsScheduler] Error during migration without interaction",
|
||||
mockError,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("runMigrationsWithInteraction", () => {
|
||||
beforeEach(() => {
|
||||
mockAuthService.authStatusFor$.mockReturnValue(of(AuthenticationStatus.Unlocked));
|
||||
mockEncryptedMigrator.needsMigrations.mockResolvedValue("needsMigrationWithMasterPassword");
|
||||
});
|
||||
|
||||
it("should skip if migration was dismissed recently", async () => {
|
||||
const recentDismissDate = new Date(Date.now() - 12 * 60 * 60 * 1000); // 12 hours ago
|
||||
const mockUserState = createMockUserState(recentDismissDate);
|
||||
mockStateProvider.getUser.mockReturnValue(mockUserState);
|
||||
|
||||
await service.runMigrationsIfNeeded(mockUserId);
|
||||
|
||||
expect(mockStateProvider.getUser).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
ENCRYPTED_MIGRATION_DISMISSED,
|
||||
);
|
||||
expect(mockLogService.info).toHaveBeenCalledWith(
|
||||
"[EncryptedMigrationsScheduler] Migration prompt dismissed recently, skipping for now.",
|
||||
);
|
||||
expect(PromptMigrationPasswordComponent.open).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should prompt for migration if dismissed date is older than 24 hours", async () => {
|
||||
const oldDismissDate = new Date(Date.now() - 25 * 60 * 60 * 1000); // 25 hours ago
|
||||
const mockUserState = createMockUserState(oldDismissDate);
|
||||
mockStateProvider.getUser.mockReturnValue(mockUserState);
|
||||
|
||||
await service.runMigrationsIfNeeded(mockUserId);
|
||||
|
||||
expect(mockStateProvider.getUser).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
ENCRYPTED_MIGRATION_DISMISSED,
|
||||
);
|
||||
expect(PromptMigrationPasswordComponent.open).toHaveBeenCalledWith(mockDialogService);
|
||||
expect(mockEncryptedMigrator.runMigrations).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
mockMasterPassword,
|
||||
);
|
||||
});
|
||||
|
||||
it("should prompt for migration if no dismiss date exists", async () => {
|
||||
const mockUserState = createMockUserState(null);
|
||||
mockStateProvider.getUser.mockReturnValue(mockUserState);
|
||||
|
||||
await service.runMigrationsIfNeeded(mockUserId);
|
||||
|
||||
expect(PromptMigrationPasswordComponent.open).toHaveBeenCalledWith(mockDialogService);
|
||||
expect(mockEncryptedMigrator.runMigrations).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
mockMasterPassword,
|
||||
);
|
||||
});
|
||||
|
||||
it("should set dismiss date when empty password is provided", async () => {
|
||||
const mockUserState = createMockUserState(null);
|
||||
mockStateProvider.getUser.mockReturnValue(mockUserState);
|
||||
|
||||
const mockDialogRef = {
|
||||
closed: of(""), // Empty password
|
||||
};
|
||||
jest.spyOn(PromptMigrationPasswordComponent, "open").mockReturnValue(mockDialogRef as any);
|
||||
|
||||
await service.runMigrationsIfNeeded(mockUserId);
|
||||
|
||||
expect(PromptMigrationPasswordComponent.open).toHaveBeenCalledWith(mockDialogService);
|
||||
expect(mockEncryptedMigrator.runMigrations).not.toHaveBeenCalled();
|
||||
expect(mockStateProvider.setUserState).toHaveBeenCalledWith(
|
||||
ENCRYPTED_MIGRATION_DISMISSED,
|
||||
expect.any(Date),
|
||||
mockUserId,
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle errors during migration prompt and show toast", async () => {
|
||||
const mockUserState = createMockUserState(null);
|
||||
mockStateProvider.getUser.mockReturnValue(mockUserState);
|
||||
|
||||
const mockError = new Error("Migration failed");
|
||||
mockEncryptedMigrator.runMigrations.mockRejectedValue(mockError);
|
||||
|
||||
await service.runMigrationsIfNeeded(mockUserId);
|
||||
|
||||
expect(PromptMigrationPasswordComponent.open).toHaveBeenCalledWith(mockDialogService);
|
||||
expect(mockEncryptedMigrator.runMigrations).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
mockMasterPassword,
|
||||
);
|
||||
expect(mockLogService.error).toHaveBeenCalledWith(
|
||||
"[EncryptedMigrationsScheduler] Error during migration prompt",
|
||||
mockError,
|
||||
);
|
||||
expect(mockToastService.showToast).toHaveBeenCalledWith({
|
||||
variant: "error",
|
||||
message: "translated_migrationsFailed",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,186 @@
|
||||
import { NavigationEnd, Router } from "@angular/router";
|
||||
import {
|
||||
combineLatest,
|
||||
switchMap,
|
||||
of,
|
||||
firstValueFrom,
|
||||
filter,
|
||||
concatMap,
|
||||
Observable,
|
||||
map,
|
||||
} from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { EncryptedMigrator } from "@bitwarden/common/key-management/encrypted-migrator/encrypted-migrator.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import {
|
||||
UserKeyDefinition,
|
||||
ENCRYPTED_MIGRATION_DISK,
|
||||
StateProvider,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { LogService } from "@bitwarden/logging";
|
||||
|
||||
import { EncryptedMigrationsSchedulerService } from "./encrypted-migrations-scheduler.service.abstraction";
|
||||
import { PromptMigrationPasswordComponent } from "./prompt-migration-password.component";
|
||||
|
||||
export const ENCRYPTED_MIGRATION_DISMISSED = new UserKeyDefinition<Date>(
|
||||
ENCRYPTED_MIGRATION_DISK,
|
||||
"encryptedMigrationDismissed",
|
||||
{
|
||||
deserializer: (obj: string) => (obj != null ? new Date(obj) : null),
|
||||
clearOn: [],
|
||||
},
|
||||
);
|
||||
const DISMISS_TIME_HOURS = 24;
|
||||
const VAULT_ROUTES = ["/vault", "/tabs/vault", "/tabs/current"];
|
||||
|
||||
/**
|
||||
* This services schedules encrypted migrations for users on clients that are interactive (non-cli), and handles manual interaction,
|
||||
* if it is required by showing a UI prompt. It is only one means of triggering migrations, in case the user stays unlocked for a while,
|
||||
* or regularly logs in without a master-password, when the migrations do require a master-password to run.
|
||||
*/
|
||||
export class DefaultEncryptedMigrationsSchedulerService implements EncryptedMigrationsSchedulerService {
|
||||
isMigrating = false;
|
||||
url$: Observable<string>;
|
||||
|
||||
constructor(
|
||||
private syncService: SyncService,
|
||||
private accountService: AccountService,
|
||||
private stateProvider: StateProvider,
|
||||
private encryptedMigrator: EncryptedMigrator,
|
||||
private authService: AuthService,
|
||||
private logService: LogService,
|
||||
private dialogService: DialogService,
|
||||
private toastService: ToastService,
|
||||
private i18nService: I18nService,
|
||||
private router: Router,
|
||||
) {
|
||||
this.url$ = this.router.events.pipe(
|
||||
filter((event: any) => event instanceof NavigationEnd),
|
||||
map((event: NavigationEnd) => event.url),
|
||||
);
|
||||
|
||||
// For all accounts, if the auth status changes to unlocked or a sync happens, prompt for migration
|
||||
this.accountService.accounts$
|
||||
.pipe(
|
||||
switchMap((accounts) => {
|
||||
const userIds = Object.keys(accounts) as UserId[];
|
||||
|
||||
if (userIds.length === 0) {
|
||||
return of([]);
|
||||
}
|
||||
|
||||
return combineLatest(
|
||||
userIds.map((userId) =>
|
||||
combineLatest([
|
||||
this.authService.authStatusFor$(userId),
|
||||
this.syncService.lastSync$(userId).pipe(filter((lastSync) => lastSync != null)),
|
||||
this.url$,
|
||||
]).pipe(
|
||||
filter(
|
||||
([authStatus, _date, url]) =>
|
||||
authStatus === AuthenticationStatus.Unlocked && VAULT_ROUTES.includes(url),
|
||||
),
|
||||
concatMap(() => this.runMigrationsIfNeeded(userId)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
async runMigrationsIfNeeded(userId: UserId): Promise<void> {
|
||||
const authStatus = await firstValueFrom(this.authService.authStatusFor$(userId));
|
||||
if (authStatus !== AuthenticationStatus.Unlocked) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isMigrating || this.encryptedMigrator.isRunningMigrations()) {
|
||||
this.logService.info(
|
||||
`[EncryptedMigrationsScheduler] Skipping migration check for user ${userId} because migrations are already in progress`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.isMigrating = true;
|
||||
switch (await this.encryptedMigrator.needsMigrations(userId)) {
|
||||
case "noMigrationNeeded":
|
||||
this.logService.info(
|
||||
`[EncryptedMigrationsScheduler] No migrations needed for user ${userId}`,
|
||||
);
|
||||
break;
|
||||
case "needsMigrationWithMasterPassword":
|
||||
this.logService.info(
|
||||
`[EncryptedMigrationsScheduler] User ${userId} needs migrations with master password`,
|
||||
);
|
||||
// If the user is unlocked, we can run migrations with the master password
|
||||
await this.runMigrationsWithInteraction(userId);
|
||||
break;
|
||||
case "needsMigration":
|
||||
this.logService.info(
|
||||
`[EncryptedMigrationsScheduler] User ${userId} needs migrations with master password`,
|
||||
);
|
||||
// If the user is unlocked, we can prompt for the master password
|
||||
await this.runMigrationsWithoutInteraction(userId);
|
||||
break;
|
||||
}
|
||||
this.isMigrating = false;
|
||||
}
|
||||
|
||||
private async runMigrationsWithoutInteraction(userId: UserId): Promise<void> {
|
||||
try {
|
||||
await this.encryptedMigrator.runMigrations(userId, null);
|
||||
} catch (error) {
|
||||
this.logService.error(
|
||||
"[EncryptedMigrationsScheduler] Error during migration without interaction",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async runMigrationsWithInteraction(userId: UserId): Promise<void> {
|
||||
// A dialog can be dismissed for a certain amount of time
|
||||
const dismissedDate = await firstValueFrom(
|
||||
this.stateProvider.getUser(userId, ENCRYPTED_MIGRATION_DISMISSED).state$,
|
||||
);
|
||||
if (dismissedDate != null) {
|
||||
const now = new Date();
|
||||
const timeDiff = now.getTime() - (dismissedDate as Date).getTime();
|
||||
const hoursDiff = timeDiff / (1000 * 60 * 60);
|
||||
|
||||
if (hoursDiff < DISMISS_TIME_HOURS) {
|
||||
this.logService.info(
|
||||
"[EncryptedMigrationsScheduler] Migration prompt dismissed recently, skipping for now.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const dialog = PromptMigrationPasswordComponent.open(this.dialogService);
|
||||
const masterPassword = await firstValueFrom(dialog.closed);
|
||||
if (Utils.isNullOrWhitespace(masterPassword)) {
|
||||
await this.stateProvider.setUserState(ENCRYPTED_MIGRATION_DISMISSED, new Date(), userId);
|
||||
} else {
|
||||
await this.encryptedMigrator.runMigrations(
|
||||
userId,
|
||||
masterPassword === undefined ? null : masterPassword,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.error("[EncryptedMigrationsScheduler] Error during migration prompt", error);
|
||||
// If migrations failed when the user actively was prompted, show a toast
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
message: this.i18nService.t("migrationsFailed"),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<form [bitSubmit]="submit" [formGroup]="migrationPasswordForm">
|
||||
<bit-dialog>
|
||||
<div class="tw-font-semibold" bitDialogTitle>
|
||||
{{ "updateEncryptionSettingsTitle" | i18n }}
|
||||
</div>
|
||||
<div bitDialogContent>
|
||||
<p>
|
||||
{{ "updateEncryptionSettingsDesc" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
href="https://bitwarden.com/help/kdf-algorithms/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label="external link"
|
||||
>
|
||||
{{ "learnMore" | i18n }}
|
||||
<i class="bwi bwi-external-link" aria-hidden="true"></i>
|
||||
</a>
|
||||
</p>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "masterPass" | i18n }}</bit-label>
|
||||
<bit-hint>{{ "confirmIdentityToContinue" | i18n }}</bit-hint>
|
||||
<input
|
||||
class="tw-font-mono"
|
||||
bitInput
|
||||
type="password"
|
||||
formControlName="masterPassword"
|
||||
[attr.title]="'masterPass' | i18n"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton
|
||||
bitSuffix
|
||||
bitPasswordInputToggle
|
||||
[attr.title]="'toggleVisibility' | i18n"
|
||||
[attr.aria-label]="'toggleVisibility' | i18n"
|
||||
></button>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<ng-container bitDialogFooter>
|
||||
<button
|
||||
type="submit"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="primary"
|
||||
[disabled]="migrationPasswordForm.invalid"
|
||||
>
|
||||
<span>{{ "updateSettings" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" bitButton bitFormButton buttonType="secondary" bitDialogClose>
|
||||
{{ "later" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
@@ -0,0 +1,82 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, inject, ChangeDetectionStrategy } from "@angular/core";
|
||||
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||
import { filter, firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { MasterPasswordUnlockService } from "@bitwarden/common/key-management/master-password/abstractions/master-password-unlock.service";
|
||||
import {
|
||||
LinkModule,
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
DialogRef,
|
||||
DialogService,
|
||||
FormFieldModule,
|
||||
IconButtonModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
/**
|
||||
* This is a generic prompt to run encryption migrations that require the master password.
|
||||
*/
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: "prompt-migration-password.component.html",
|
||||
imports: [
|
||||
DialogModule,
|
||||
LinkModule,
|
||||
CommonModule,
|
||||
JslibModule,
|
||||
ButtonModule,
|
||||
IconButtonModule,
|
||||
ReactiveFormsModule,
|
||||
AsyncActionsModule,
|
||||
FormFieldModule,
|
||||
],
|
||||
})
|
||||
export class PromptMigrationPasswordComponent {
|
||||
private dialogRef = inject(DialogRef<string>);
|
||||
private formBuilder = inject(FormBuilder);
|
||||
private masterPasswordUnlockService = inject(MasterPasswordUnlockService);
|
||||
private accountService = inject(AccountService);
|
||||
|
||||
migrationPasswordForm = this.formBuilder.group({
|
||||
masterPassword: ["", [Validators.required]],
|
||||
});
|
||||
|
||||
static open(dialogService: DialogService) {
|
||||
return dialogService.open<string>(PromptMigrationPasswordComponent);
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
const masterPasswordControl = this.migrationPasswordForm.controls.masterPassword;
|
||||
|
||||
if (!masterPasswordControl.value || masterPasswordControl.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { userId } = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
filter((account) => account != null),
|
||||
map((account) => {
|
||||
return {
|
||||
userId: account!.id,
|
||||
};
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
if (
|
||||
!(await this.masterPasswordUnlockService.proofOfDecryption(
|
||||
masterPasswordControl.value,
|
||||
userId,
|
||||
))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Return the master password to the caller
|
||||
this.dialogRef.close(masterPasswordControl.value);
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { ErrorHandler, LOCALE_ID, NgModule } from "@angular/core";
|
||||
import { APP_INITIALIZER, ErrorHandler, LOCALE_ID, NgModule } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
@@ -167,6 +168,8 @@ import { OrganizationBillingService } from "@bitwarden/common/billing/services/o
|
||||
import { DefaultSubscriptionPricingService } from "@bitwarden/common/billing/services/subscription-pricing.service";
|
||||
import { HibpApiService } from "@bitwarden/common/dirt/services/hibp-api.service";
|
||||
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
|
||||
import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service";
|
||||
import { DefaultAccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/default-account-cryptographic-state.service";
|
||||
import {
|
||||
DefaultKeyGenerationService,
|
||||
KeyGenerationService,
|
||||
@@ -177,11 +180,15 @@ import { EncryptServiceImplementation } from "@bitwarden/common/key-management/c
|
||||
import { WebCryptoFunctionService } from "@bitwarden/common/key-management/crypto/services/web-crypto-function.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { DeviceTrustService } from "@bitwarden/common/key-management/device-trust/services/device-trust.service.implementation";
|
||||
import { DefaultEncryptedMigrator } from "@bitwarden/common/key-management/encrypted-migrator/default-encrypted-migrator";
|
||||
import { EncryptedMigrator } from "@bitwarden/common/key-management/encrypted-migrator/encrypted-migrator.abstraction";
|
||||
import { DefaultChangeKdfApiService } from "@bitwarden/common/key-management/kdf/change-kdf-api.service";
|
||||
import { ChangeKdfApiService } from "@bitwarden/common/key-management/kdf/change-kdf-api.service.abstraction";
|
||||
import { DefaultChangeKdfService } from "@bitwarden/common/key-management/kdf/change-kdf-service";
|
||||
import { ChangeKdfService } from "@bitwarden/common/key-management/kdf/change-kdf-service.abstraction";
|
||||
import { DefaultChangeKdfService } from "@bitwarden/common/key-management/kdf/change-kdf.service";
|
||||
import { ChangeKdfService } from "@bitwarden/common/key-management/kdf/change-kdf.service.abstraction";
|
||||
import { KeyConnectorApiService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector-api.service";
|
||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { DefaultKeyConnectorApiService } from "@bitwarden/common/key-management/key-connector/services/default-key-connector-api.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/services/key-connector.service";
|
||||
import { KeyApiService } from "@bitwarden/common/key-management/keys/services/abstractions/key-api-service.abstraction";
|
||||
import { RotateableKeySetService } from "@bitwarden/common/key-management/keys/services/abstractions/rotateable-key-set.service";
|
||||
@@ -204,6 +211,7 @@ import {
|
||||
SendPasswordService,
|
||||
DefaultSendPasswordService,
|
||||
} from "@bitwarden/common/key-management/sends";
|
||||
import { SessionTimeoutTypeService } from "@bitwarden/common/key-management/session-timeout";
|
||||
import {
|
||||
DefaultVaultTimeoutService,
|
||||
DefaultVaultTimeoutSettingsService,
|
||||
@@ -223,6 +231,7 @@ import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platfor
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service";
|
||||
import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory";
|
||||
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
@@ -261,6 +270,7 @@ import { FileUploadService } from "@bitwarden/common/platform/services/file-uplo
|
||||
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||
import { DefaultSdkService } from "@bitwarden/common/platform/services/sdk/default-sdk.service";
|
||||
import { DefaultRegisterSdkService } from "@bitwarden/common/platform/services/sdk/register-sdk.service";
|
||||
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
|
||||
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/services/validation.service";
|
||||
@@ -326,6 +336,7 @@ import { DefaultTaskService, TaskService } from "@bitwarden/common/vault/tasks";
|
||||
import {
|
||||
AnonLayoutWrapperDataService,
|
||||
DefaultAnonLayoutWrapperDataService,
|
||||
DialogService,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import {
|
||||
@@ -392,6 +403,8 @@ import { DeviceTrustToastService as DeviceTrustToastServiceAbstraction } from ".
|
||||
import { DeviceTrustToastService } from "../auth/services/device-trust-toast.service.implementation";
|
||||
import { NoopPremiumInterestStateService } from "../billing/services/premium-interest/noop-premium-interest-state.service";
|
||||
import { PremiumInterestStateService } from "../billing/services/premium-interest/premium-interest-state.service.abstraction";
|
||||
import { DefaultEncryptedMigrationsSchedulerService } from "../key-management/encrypted-migration/encrypted-migrations-scheduler.service";
|
||||
import { EncryptedMigrationsSchedulerService } from "../key-management/encrypted-migration/encrypted-migrations-scheduler.service.abstraction";
|
||||
import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service";
|
||||
import { DocumentLangSetter } from "../platform/i18n";
|
||||
import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service";
|
||||
@@ -512,6 +525,23 @@ const safeProviders: SafeProvider[] = [
|
||||
TokenServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ChangeKdfService,
|
||||
useClass: DefaultChangeKdfService,
|
||||
deps: [ChangeKdfApiService, SdkService, KeyService, InternalMasterPasswordServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: EncryptedMigrator,
|
||||
useClass: DefaultEncryptedMigrator,
|
||||
deps: [
|
||||
KdfConfigService,
|
||||
ChangeKdfService,
|
||||
LogService,
|
||||
ConfigService,
|
||||
MasterPasswordServiceAbstraction,
|
||||
SyncService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: LoginStrategyServiceAbstraction,
|
||||
useClass: LoginStrategyService,
|
||||
@@ -542,6 +572,7 @@ const safeProviders: SafeProvider[] = [
|
||||
KdfConfigService,
|
||||
TaskSchedulerService,
|
||||
ConfigService,
|
||||
AccountCryptographicStateService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -682,7 +713,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: InternalUserDecryptionOptionsServiceAbstraction,
|
||||
useClass: UserDecryptionOptionsService,
|
||||
deps: [StateProvider],
|
||||
deps: [SingleUserStateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: UserDecryptionOptionsServiceAbstraction,
|
||||
@@ -864,8 +895,14 @@ const safeProviders: SafeProvider[] = [
|
||||
StateProvider,
|
||||
SecurityStateService,
|
||||
KdfConfigService,
|
||||
AccountCryptographicStateService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AccountCryptographicStateService,
|
||||
useClass: DefaultAccountCryptographicStateService,
|
||||
deps: [StateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: BroadcasterService,
|
||||
useClass: DefaultBroadcasterService,
|
||||
@@ -885,6 +922,7 @@ const safeProviders: SafeProvider[] = [
|
||||
StateProvider,
|
||||
LogService,
|
||||
DEFAULT_VAULT_TIMEOUT,
|
||||
SessionTimeoutTypeService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -921,7 +959,7 @@ const safeProviders: SafeProvider[] = [
|
||||
deps: [
|
||||
FolderServiceAbstraction,
|
||||
CipherServiceAbstraction,
|
||||
PinServiceAbstraction,
|
||||
KeyGenerationService,
|
||||
KeyService,
|
||||
EncryptService,
|
||||
CryptoFunctionServiceAbstraction,
|
||||
@@ -941,7 +979,7 @@ const safeProviders: SafeProvider[] = [
|
||||
deps: [
|
||||
CipherServiceAbstraction,
|
||||
VaultExportApiService,
|
||||
PinServiceAbstraction,
|
||||
KeyGenerationService,
|
||||
KeyService,
|
||||
EncryptService,
|
||||
CryptoFunctionServiceAbstraction,
|
||||
@@ -1028,6 +1066,7 @@ const safeProviders: SafeProvider[] = [
|
||||
WebPushConnectionService,
|
||||
AuthRequestAnsweringService,
|
||||
ConfigService,
|
||||
InternalPolicyService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -1066,7 +1105,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: InternalPolicyService,
|
||||
useClass: DefaultPolicyService,
|
||||
deps: [StateProvider, OrganizationServiceAbstraction],
|
||||
deps: [StateProvider, OrganizationServiceAbstraction, AccountServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: PolicyServiceAbstraction,
|
||||
@@ -1296,6 +1335,7 @@ const safeProviders: SafeProvider[] = [
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
LogService,
|
||||
ConfigService,
|
||||
AccountServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -1306,7 +1346,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: ChangeKdfService,
|
||||
useClass: DefaultChangeKdfService,
|
||||
deps: [ChangeKdfApiService, SdkService],
|
||||
deps: [ChangeKdfApiService, SdkService, KeyService, InternalMasterPasswordServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AuthRequestServiceAbstraction,
|
||||
@@ -1330,16 +1370,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: PinServiceAbstraction,
|
||||
useClass: PinService,
|
||||
deps: [
|
||||
AccountServiceAbstraction,
|
||||
EncryptService,
|
||||
KdfConfigService,
|
||||
KeyGenerationService,
|
||||
LogService,
|
||||
KeyService,
|
||||
SdkService,
|
||||
PinStateServiceAbstraction,
|
||||
],
|
||||
deps: [EncryptService, LogService, KeyService, SdkService, PinStateServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: WebAuthnLoginPrfKeyServiceAbstraction,
|
||||
@@ -1473,7 +1504,13 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: SubscriptionPricingServiceAbstraction,
|
||||
useClass: DefaultSubscriptionPricingService,
|
||||
deps: [BillingApiServiceAbstraction, ConfigService, I18nServiceAbstraction, LogService],
|
||||
deps: [
|
||||
BillingApiServiceAbstraction,
|
||||
ConfigService,
|
||||
I18nServiceAbstraction,
|
||||
LogService,
|
||||
EnvironmentService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: OrganizationManagementPreferencesService,
|
||||
@@ -1541,6 +1578,7 @@ const safeProviders: SafeProvider[] = [
|
||||
OrganizationApiServiceAbstraction,
|
||||
OrganizationUserApiService,
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
AccountCryptographicStateService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -1589,6 +1627,19 @@ const safeProviders: SafeProvider[] = [
|
||||
SsoLoginServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: RegisterSdkService,
|
||||
useClass: DefaultRegisterSdkService,
|
||||
deps: [
|
||||
SdkClientFactory,
|
||||
EnvironmentService,
|
||||
PlatformUtilsServiceAbstraction,
|
||||
AccountServiceAbstraction,
|
||||
ApiServiceAbstraction,
|
||||
StateProvider,
|
||||
ConfigService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SdkService,
|
||||
useClass: DefaultSdkService,
|
||||
@@ -1647,6 +1698,7 @@ const safeProviders: SafeProvider[] = [
|
||||
SsoLoginServiceAbstraction,
|
||||
SyncService,
|
||||
UserAsymmetricKeysRegenerationService,
|
||||
EncryptedMigrator,
|
||||
LogService,
|
||||
],
|
||||
}),
|
||||
@@ -1717,6 +1769,28 @@ const safeProviders: SafeProvider[] = [
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: EncryptedMigrationsSchedulerService,
|
||||
useClass: DefaultEncryptedMigrationsSchedulerService,
|
||||
deps: [
|
||||
SyncService,
|
||||
AccountService,
|
||||
StateProvider,
|
||||
EncryptedMigrator,
|
||||
AuthServiceAbstraction,
|
||||
LogService,
|
||||
DialogService,
|
||||
ToastService,
|
||||
I18nServiceAbstraction,
|
||||
Router,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: APP_INITIALIZER as SafeInjectionToken<() => Promise<void>>,
|
||||
useFactory: (encryptedMigrationsScheduler: EncryptedMigrationsSchedulerService) => () => {},
|
||||
deps: [EncryptedMigrationsSchedulerService],
|
||||
multi: true,
|
||||
}),
|
||||
safeProvider({
|
||||
provide: LockService,
|
||||
useClass: DefaultLockService,
|
||||
@@ -1763,6 +1837,11 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: IpcSessionRepository,
|
||||
deps: [StateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: KeyConnectorApiService,
|
||||
useClass: DefaultKeyConnectorApiService,
|
||||
deps: [ApiServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: PremiumInterestStateService,
|
||||
useClass: NoopPremiumInterestStateService,
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
<!-- Applying width and height styles directly to synchronize icon sizing between web/browser/desktop -->
|
||||
<div
|
||||
class="tw-flex tw-justify-center tw-items-center"
|
||||
[ngStyle]="coloredIcon() ? { width: '36px', height: '36px' } : {}"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div class="tw-flex tw-justify-center tw-items-center" [ngStyle]="iconStyle()" aria-hidden="true">
|
||||
<ng-container *ngIf="data$ | async as data">
|
||||
@if (data.imageEnabled && data.image) {
|
||||
<img
|
||||
@@ -16,7 +12,7 @@
|
||||
'tw-invisible tw-absolute': !imageLoaded(),
|
||||
'tw-size-6': !coloredIcon(),
|
||||
}"
|
||||
[ngStyle]="coloredIcon() ? { width: '36px', height: '36px' } : {}"
|
||||
[ngStyle]="iconStyle()"
|
||||
(load)="imageLoaded.set(true)"
|
||||
(error)="imageLoaded.set(false)"
|
||||
/>
|
||||
@@ -28,7 +24,7 @@
|
||||
'tw-bg-illustration-bg-primary tw-rounded-full':
|
||||
data.icon?.startsWith('bwi-') && coloredIcon(),
|
||||
}"
|
||||
[ngStyle]="coloredIcon() ? { width: '36px', height: '36px' } : {}"
|
||||
[ngStyle]="iconStyle()"
|
||||
>
|
||||
<i
|
||||
class="tw-text-muted bwi bwi-lg {{ data.icon }}"
|
||||
@@ -36,6 +32,7 @@
|
||||
color: coloredIcon() ? 'rgb(var(--color-illustration-outline))' : null,
|
||||
width: data.icon?.startsWith('credit-card') && coloredIcon() ? '36px' : null,
|
||||
height: data.icon?.startsWith('credit-card') && coloredIcon() ? '30px' : null,
|
||||
fontSize: size() ? size() + 'px' : null,
|
||||
}"
|
||||
></i>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChangeDetectionStrategy, Component, input, signal } from "@angular/core";
|
||||
import { ChangeDetectionStrategy, Component, computed, input, signal } from "@angular/core";
|
||||
import { toObservable } from "@angular/core/rxjs-interop";
|
||||
import {
|
||||
combineLatest,
|
||||
@@ -32,8 +32,32 @@ export class IconComponent {
|
||||
*/
|
||||
readonly coloredIcon = input<boolean>(false);
|
||||
|
||||
/**
|
||||
* Optional custom size for the icon in pixels.
|
||||
* When provided, forces explicit dimensions on the icon wrapper to prevent layout collapse at different zoom levels.
|
||||
* If not provided, the wrapper has no explicit dimensions and relies on CSS classes (tw-size-6/24px for images).
|
||||
* This can cause the wrapper to collapse when images are loading/hidden, especially at high browser zoom levels.
|
||||
* Reference: default image size is tw-size-6 (24px), coloredIcon uses 36px.
|
||||
*/
|
||||
readonly size = input<number>();
|
||||
|
||||
readonly imageLoaded = signal(false);
|
||||
|
||||
/**
|
||||
* Computed style object for icon dimensions.
|
||||
* Centralizes the sizing logic to avoid repetition in the template.
|
||||
*/
|
||||
protected readonly iconStyle = computed(() => {
|
||||
if (this.coloredIcon()) {
|
||||
return { width: "36px", height: "36px" };
|
||||
}
|
||||
const size = this.size();
|
||||
if (size) {
|
||||
return { width: size + "px", height: size + "px" };
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
protected data$: Observable<CipherIconDetails>;
|
||||
|
||||
constructor(
|
||||
|
||||
@@ -88,14 +88,10 @@ export class VaultFilterComponent implements OnInit {
|
||||
this.folders$ = await this.vaultFilterService.buildNestedFolders();
|
||||
this.collections = await this.initCollections();
|
||||
|
||||
const userCanArchive = await firstValueFrom(
|
||||
this.cipherArchiveService.userCanArchive$(this.activeUserId),
|
||||
);
|
||||
const showArchiveVault = await firstValueFrom(
|
||||
this.cipherArchiveService.showArchiveVault$(this.activeUserId),
|
||||
this.showArchiveVaultFilter = await firstValueFrom(
|
||||
this.cipherArchiveService.hasArchiveFlagEnabled$,
|
||||
);
|
||||
|
||||
this.showArchiveVaultFilter = userCanArchive || showArchiveVault;
|
||||
this.isLoaded = true;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user