mirror of
https://github.com/bitwarden/browser
synced 2026-02-21 20:04:02 +00:00
Merge branch 'main' into km/auto-kdf-qa
This commit is contained in:
@@ -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";
|
||||
@@ -48,10 +49,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({
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -2,14 +2,13 @@ import { Router } from "@angular/router";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { AccountInfo } 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 { SingleUserState, StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { FakeAccountService } from "@bitwarden/common/spec";
|
||||
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";
|
||||
@@ -22,17 +21,15 @@ import { PromptMigrationPasswordComponent } from "./prompt-migration-password.co
|
||||
|
||||
const SomeUser = "SomeUser" as UserId;
|
||||
const AnotherUser = "SomeOtherUser" as UserId;
|
||||
const accounts: Record<UserId, AccountInfo> = {
|
||||
[SomeUser]: {
|
||||
const accounts = {
|
||||
[SomeUser]: mockAccountInfoWith({
|
||||
name: "some user",
|
||||
email: "some.user@example.com",
|
||||
emailVerified: true,
|
||||
},
|
||||
[AnotherUser]: {
|
||||
}),
|
||||
[AnotherUser]: mockAccountInfoWith({
|
||||
name: "some other user",
|
||||
email: "some.other.user@example.com",
|
||||
emailVerified: true,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
describe("DefaultEncryptedMigrationsSchedulerService", () => {
|
||||
|
||||
@@ -5,8 +5,7 @@ import { filter, firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
||||
import { MasterPasswordUnlockService } from "@bitwarden/common/key-management/master-password/abstractions/master-password-unlock.service";
|
||||
import {
|
||||
LinkModule,
|
||||
AsyncActionsModule,
|
||||
@@ -39,7 +38,7 @@ import {
|
||||
export class PromptMigrationPasswordComponent {
|
||||
private dialogRef = inject(DialogRef<string>);
|
||||
private formBuilder = inject(FormBuilder);
|
||||
private uvService = inject(UserVerificationService);
|
||||
private masterPasswordUnlockService = inject(MasterPasswordUnlockService);
|
||||
private accountService = inject(AccountService);
|
||||
|
||||
migrationPasswordForm = this.formBuilder.group({
|
||||
@@ -57,23 +56,21 @@ export class PromptMigrationPasswordComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
const { userId, email } = await firstValueFrom(
|
||||
const { userId } = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
filter((account) => account != null),
|
||||
map((account) => {
|
||||
return {
|
||||
userId: account!.id,
|
||||
email: account!.email,
|
||||
};
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
if (
|
||||
!(await this.uvService.verifyUserByMasterPassword(
|
||||
{ type: VerificationType.MasterPassword, secret: masterPasswordControl.value },
|
||||
!(await this.masterPasswordUnlockService.proofOfDecryption(
|
||||
masterPasswordControl.value,
|
||||
userId,
|
||||
email,
|
||||
))
|
||||
) {
|
||||
return;
|
||||
|
||||
@@ -189,6 +189,7 @@ export abstract class LoginStrategy {
|
||||
name: accountInformation.name,
|
||||
email: accountInformation.email ?? "",
|
||||
emailVerified: accountInformation.email_verified ?? false,
|
||||
creationDate: undefined, // We don't get a creation date in the token. See https://bitwarden.atlassian.net/browse/PM-29551 for consolidation plans.
|
||||
});
|
||||
|
||||
// User env must be seeded from currently set env before switching to the account
|
||||
|
||||
@@ -8,7 +8,7 @@ import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { SystemService } from "@bitwarden/common/platform/abstractions/system.service";
|
||||
import { mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { mockAccountServiceWith, mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
@@ -79,17 +79,21 @@ describe("DefaultLockService", () => {
|
||||
);
|
||||
|
||||
it("locks the active account last", async () => {
|
||||
await accountService.addAccount(mockUser2, {
|
||||
name: "name2",
|
||||
email: "email2@example.com",
|
||||
emailVerified: false,
|
||||
});
|
||||
await accountService.addAccount(
|
||||
mockUser2,
|
||||
mockAccountInfoWith({
|
||||
name: "name2",
|
||||
email: "email2@example.com",
|
||||
}),
|
||||
);
|
||||
|
||||
await accountService.addAccount(mockUser3, {
|
||||
name: "name3",
|
||||
email: "name3@example.com",
|
||||
emailVerified: false,
|
||||
});
|
||||
await accountService.addAccount(
|
||||
mockUser3,
|
||||
mockAccountInfoWith({
|
||||
name: "name3",
|
||||
email: "name3@example.com",
|
||||
}),
|
||||
);
|
||||
|
||||
const lockSpy = jest.spyOn(sut, "lock").mockResolvedValue(undefined);
|
||||
|
||||
|
||||
@@ -6,19 +6,26 @@ import { ReplaySubject, combineLatest, map, Observable } from "rxjs";
|
||||
import { Account, AccountInfo, AccountService } from "../src/auth/abstractions/account.service";
|
||||
import { UserId } from "../src/types/guid";
|
||||
|
||||
/**
|
||||
* Creates a mock AccountInfo object with sensible defaults that can be overridden.
|
||||
* Use this when you need just an AccountInfo object in tests.
|
||||
*/
|
||||
export function mockAccountInfoWith(info: Partial<AccountInfo> = {}): AccountInfo {
|
||||
return {
|
||||
name: "name",
|
||||
email: "email",
|
||||
emailVerified: true,
|
||||
creationDate: "2024-01-01T00:00:00.000Z",
|
||||
...info,
|
||||
};
|
||||
}
|
||||
|
||||
export function mockAccountServiceWith(
|
||||
userId: UserId,
|
||||
info: Partial<AccountInfo> = {},
|
||||
activity: Record<UserId, Date> = {},
|
||||
): FakeAccountService {
|
||||
const fullInfo: AccountInfo = {
|
||||
...info,
|
||||
...{
|
||||
name: "name",
|
||||
email: "email",
|
||||
emailVerified: true,
|
||||
},
|
||||
};
|
||||
const fullInfo = mockAccountInfoWith(info);
|
||||
|
||||
const fullActivity = { [userId]: new Date(), ...activity };
|
||||
|
||||
@@ -104,6 +111,10 @@ export class FakeAccountService implements AccountService {
|
||||
await this.mock.setAccountEmailVerified(userId, emailVerified);
|
||||
}
|
||||
|
||||
async setAccountCreationDate(userId: UserId, creationDate: string): Promise<void> {
|
||||
await this.mock.setAccountCreationDate(userId, creationDate);
|
||||
}
|
||||
|
||||
async switchAccount(userId: UserId): Promise<void> {
|
||||
const next =
|
||||
userId == null ? null : { id: userId, ...this.accountsSubject["_buffer"]?.[0]?.[userId] };
|
||||
@@ -127,4 +138,5 @@ const loggedOutInfo: AccountInfo = {
|
||||
name: undefined,
|
||||
email: "",
|
||||
emailVerified: false,
|
||||
creationDate: undefined,
|
||||
};
|
||||
|
||||
@@ -2,14 +2,11 @@ import { Observable } from "rxjs";
|
||||
|
||||
import { UserId } from "../../types/guid";
|
||||
|
||||
/**
|
||||
* Holds information about an account for use in the AccountService
|
||||
* if more information is added, be sure to update the equality method.
|
||||
*/
|
||||
export type AccountInfo = {
|
||||
email: string;
|
||||
emailVerified: boolean;
|
||||
name: string | undefined;
|
||||
creationDate: string | undefined;
|
||||
};
|
||||
|
||||
export type Account = { id: UserId } & AccountInfo;
|
||||
@@ -75,6 +72,12 @@ export abstract class AccountService {
|
||||
* @param emailVerified
|
||||
*/
|
||||
abstract setAccountEmailVerified(userId: UserId, emailVerified: boolean): Promise<void>;
|
||||
/**
|
||||
* updates the `accounts$` observable with the creation date for the account.
|
||||
* @param userId
|
||||
* @param creationDate
|
||||
*/
|
||||
abstract setAccountCreationDate(userId: UserId, creationDate: string): Promise<void>;
|
||||
/**
|
||||
* updates the `accounts$` observable with the new VerifyNewDeviceLogin property for the account.
|
||||
* @param userId
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { mockAccountInfoWith } from "../../../spec/fake-account-service";
|
||||
import { FakeGlobalState } from "../../../spec/fake-state";
|
||||
import {
|
||||
FakeGlobalStateProvider,
|
||||
@@ -27,7 +28,7 @@ import {
|
||||
} from "./account.service";
|
||||
|
||||
describe("accountInfoEqual", () => {
|
||||
const accountInfo: AccountInfo = { name: "name", email: "email", emailVerified: true };
|
||||
const accountInfo = mockAccountInfoWith();
|
||||
|
||||
it("compares nulls", () => {
|
||||
expect(accountInfoEqual(null, null)).toBe(true);
|
||||
@@ -64,6 +65,23 @@ describe("accountInfoEqual", () => {
|
||||
expect(accountInfoEqual(accountInfo, same)).toBe(true);
|
||||
expect(accountInfoEqual(accountInfo, different)).toBe(false);
|
||||
});
|
||||
|
||||
it("compares creationDate", () => {
|
||||
const same = { ...accountInfo };
|
||||
const different = { ...accountInfo, creationDate: "2024-12-31T00:00:00.000Z" };
|
||||
|
||||
expect(accountInfoEqual(accountInfo, same)).toBe(true);
|
||||
expect(accountInfoEqual(accountInfo, different)).toBe(false);
|
||||
});
|
||||
|
||||
it("compares undefined creationDate", () => {
|
||||
const accountWithoutCreationDate = mockAccountInfoWith({ creationDate: undefined });
|
||||
const same = { ...accountWithoutCreationDate };
|
||||
const different = { ...accountWithoutCreationDate, creationDate: "2024-01-01T00:00:00.000Z" };
|
||||
|
||||
expect(accountInfoEqual(accountWithoutCreationDate, same)).toBe(true);
|
||||
expect(accountInfoEqual(accountWithoutCreationDate, different)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("accountService", () => {
|
||||
@@ -76,7 +94,10 @@ describe("accountService", () => {
|
||||
let activeAccountIdState: FakeGlobalState<UserId>;
|
||||
let accountActivityState: FakeGlobalState<Record<UserId, Date>>;
|
||||
const userId = Utils.newGuid() as UserId;
|
||||
const userInfo = { email: "email", name: "name", emailVerified: true };
|
||||
const userInfo = mockAccountInfoWith({
|
||||
email: "email",
|
||||
name: "name",
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
messagingService = mock();
|
||||
@@ -253,6 +274,56 @@ describe("accountService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("setCreationDate", () => {
|
||||
const initialState = { [userId]: userInfo };
|
||||
beforeEach(() => {
|
||||
accountsState.stateSubject.next(initialState);
|
||||
});
|
||||
|
||||
it("should update the account with a new creation date", async () => {
|
||||
const newCreationDate = "2024-12-31T00:00:00.000Z";
|
||||
await sut.setAccountCreationDate(userId, newCreationDate);
|
||||
const currentState = await firstValueFrom(accountsState.state$);
|
||||
|
||||
expect(currentState).toEqual({
|
||||
[userId]: { ...userInfo, creationDate: newCreationDate },
|
||||
});
|
||||
});
|
||||
|
||||
it("should not update if the creation date is the same", async () => {
|
||||
await sut.setAccountCreationDate(userId, userInfo.creationDate);
|
||||
const currentState = await firstValueFrom(accountsState.state$);
|
||||
|
||||
expect(currentState).toEqual(initialState);
|
||||
});
|
||||
|
||||
it("should update from undefined to a defined creation date", async () => {
|
||||
const accountWithoutCreationDate = mockAccountInfoWith({
|
||||
...userInfo,
|
||||
creationDate: undefined,
|
||||
});
|
||||
accountsState.stateSubject.next({ [userId]: accountWithoutCreationDate });
|
||||
|
||||
const newCreationDate = "2024-06-15T12:30:00.000Z";
|
||||
await sut.setAccountCreationDate(userId, newCreationDate);
|
||||
const currentState = await firstValueFrom(accountsState.state$);
|
||||
|
||||
expect(currentState).toEqual({
|
||||
[userId]: { ...accountWithoutCreationDate, creationDate: newCreationDate },
|
||||
});
|
||||
});
|
||||
|
||||
it("should update to a different creation date string format", async () => {
|
||||
const newCreationDate = "2023-03-15T08:45:30.123Z";
|
||||
await sut.setAccountCreationDate(userId, newCreationDate);
|
||||
const currentState = await firstValueFrom(accountsState.state$);
|
||||
|
||||
expect(currentState).toEqual({
|
||||
[userId]: { ...userInfo, creationDate: newCreationDate },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("setAccountVerifyNewDeviceLogin", () => {
|
||||
const initialState = true;
|
||||
beforeEach(() => {
|
||||
@@ -294,6 +365,7 @@ describe("accountService", () => {
|
||||
email: "",
|
||||
emailVerified: false,
|
||||
name: undefined,
|
||||
creationDate: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -62,6 +62,7 @@ const LOGGED_OUT_INFO: AccountInfo = {
|
||||
email: "",
|
||||
emailVerified: false,
|
||||
name: undefined,
|
||||
creationDate: undefined,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -167,6 +168,10 @@ export class AccountServiceImplementation implements InternalAccountService {
|
||||
await this.setAccountInfo(userId, { emailVerified });
|
||||
}
|
||||
|
||||
async setAccountCreationDate(userId: UserId, creationDate: string): Promise<void> {
|
||||
await this.setAccountInfo(userId, { creationDate });
|
||||
}
|
||||
|
||||
async clean(userId: UserId) {
|
||||
await this.setAccountInfo(userId, LOGGED_OUT_INFO);
|
||||
await this.removeAccountActivity(userId);
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
SystemNotificationEvent,
|
||||
SystemNotificationsService,
|
||||
} from "@bitwarden/common/platform/system-notifications/system-notifications.service";
|
||||
import { mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
import { AuthRequestAnsweringService } from "./auth-request-answering.service";
|
||||
@@ -48,14 +49,16 @@ describe("AuthRequestAnsweringService", () => {
|
||||
|
||||
// Common defaults
|
||||
authService.activeAccountStatus$ = of(AuthenticationStatus.Locked);
|
||||
accountService.activeAccount$ = of({
|
||||
id: userId,
|
||||
const accountInfo = mockAccountInfoWith({
|
||||
email: "user@example.com",
|
||||
emailVerified: true,
|
||||
name: "User",
|
||||
});
|
||||
accountService.activeAccount$ = of({
|
||||
id: userId,
|
||||
...accountInfo,
|
||||
});
|
||||
accountService.accounts$ = of({
|
||||
[userId]: { email: "user@example.com", emailVerified: true, name: "User" },
|
||||
[userId]: accountInfo,
|
||||
});
|
||||
(masterPasswordService.forceSetPasswordReason$ as jest.Mock).mockReturnValue(
|
||||
of(ForceSetPasswordReason.None),
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
makeStaticByteArray,
|
||||
mockAccountServiceWith,
|
||||
trackEmissions,
|
||||
mockAccountInfoWith,
|
||||
} from "../../../spec";
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { MessagingService } from "../../platform/abstractions/messaging.service";
|
||||
@@ -58,9 +59,10 @@ describe("AuthService", () => {
|
||||
const accountInfo = {
|
||||
status: AuthenticationStatus.Unlocked,
|
||||
id: userId,
|
||||
email: "email",
|
||||
emailVerified: false,
|
||||
name: "name",
|
||||
...mockAccountInfoWith({
|
||||
email: "email",
|
||||
name: "name",
|
||||
}),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -112,9 +114,10 @@ describe("AuthService", () => {
|
||||
const accountInfo2 = {
|
||||
status: AuthenticationStatus.Unlocked,
|
||||
id: Utils.newGuid() as UserId,
|
||||
email: "email2",
|
||||
emailVerified: false,
|
||||
name: "name2",
|
||||
...mockAccountInfoWith({
|
||||
email: "email2",
|
||||
name: "name2",
|
||||
}),
|
||||
};
|
||||
|
||||
const emissions = trackEmissions(sut.activeAccountStatus$);
|
||||
@@ -131,11 +134,13 @@ describe("AuthService", () => {
|
||||
it("requests auth status for all known users", async () => {
|
||||
const userId2 = Utils.newGuid() as UserId;
|
||||
|
||||
await accountService.addAccount(userId2, {
|
||||
email: "email2",
|
||||
emailVerified: false,
|
||||
name: "name2",
|
||||
});
|
||||
await accountService.addAccount(
|
||||
userId2,
|
||||
mockAccountInfoWith({
|
||||
email: "email2",
|
||||
name: "name2",
|
||||
}),
|
||||
);
|
||||
|
||||
const mockFn = jest.fn().mockReturnValue(of(AuthenticationStatus.Locked));
|
||||
sut.authStatusFor$ = mockFn;
|
||||
|
||||
@@ -8,12 +8,13 @@ import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { mockAccountInfoWith } from "../../../spec/fake-account-service";
|
||||
import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationAutoEnrollStatusResponse } from "../../admin-console/models/response/organization-auto-enroll-status.response";
|
||||
import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service";
|
||||
import { I18nService } from "../../platform/abstractions/i18n.service";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { Account, AccountInfo, AccountService } from "../abstractions/account.service";
|
||||
import { Account, AccountService } from "../abstractions/account.service";
|
||||
|
||||
import { PasswordResetEnrollmentServiceImplementation } from "./password-reset-enrollment.service.implementation";
|
||||
|
||||
@@ -96,11 +97,10 @@ describe("PasswordResetEnrollmentServiceImplementation", () => {
|
||||
const encryptedKey = { encryptedString: "encryptedString" };
|
||||
organizationApiService.getKeys.mockResolvedValue(orgKeyResponse as any);
|
||||
|
||||
const user1AccountInfo: AccountInfo = {
|
||||
const user1AccountInfo = mockAccountInfoWith({
|
||||
name: "Test User 1",
|
||||
email: "test1@email.com",
|
||||
emailVerified: true,
|
||||
};
|
||||
});
|
||||
activeAccountSubject.next(Object.assign(user1AccountInfo, { id: "userId" as UserId }));
|
||||
|
||||
keyService.userKey$.mockReturnValue(of({ key: "key" } as any));
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
/**
|
||||
* Abstraction for phishing detection settings
|
||||
*/
|
||||
export abstract class PhishingDetectionSettingsServiceAbstraction {
|
||||
/**
|
||||
* An observable for whether phishing detection is available for the active user account.
|
||||
*
|
||||
* Access is granted only when the PhishingDetection feature flag is enabled and
|
||||
* at least one of the following is true for the active account:
|
||||
* - the user has a personal premium subscription
|
||||
* - the user is a member of a Family org (ProductTierType.Families)
|
||||
* - the user is a member of an Enterprise org with `usePhishingBlocker` enabled
|
||||
*
|
||||
* Note: Non-specified organization types (e.g., Team orgs) do not grant access.
|
||||
*/
|
||||
abstract readonly available$: Observable<boolean>;
|
||||
/**
|
||||
* An observable for whether phishing detection is on for the active user account
|
||||
*
|
||||
* This is true when {@link available$} is true and when {@link enabled$} is true
|
||||
*/
|
||||
abstract readonly on$: Observable<boolean>;
|
||||
/**
|
||||
* An observable for whether phishing detection is enabled
|
||||
*/
|
||||
abstract readonly enabled$: Observable<boolean>;
|
||||
/**
|
||||
* Sets whether phishing detection is enabled
|
||||
*
|
||||
* @param enabled True to enable, false to disable
|
||||
*/
|
||||
abstract setEnabled: (userId: UserId, enabled: boolean) => Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject, firstValueFrom, Subject } from "rxjs";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../../spec";
|
||||
import { UserId } from "../../../types/guid";
|
||||
|
||||
import { PhishingDetectionSettingsService } from "./phishing-detection-settings.service";
|
||||
|
||||
describe("PhishingDetectionSettingsService", () => {
|
||||
// Mock services
|
||||
let mockAccountService: MockProxy<AccountService>;
|
||||
let mockBillingService: MockProxy<BillingAccountProfileStateService>;
|
||||
let mockConfigService: MockProxy<ConfigService>;
|
||||
let mockOrganizationService: MockProxy<OrganizationService>;
|
||||
|
||||
// RxJS Subjects we control in the tests
|
||||
let activeAccountSubject: BehaviorSubject<Account | null>;
|
||||
let featureFlagSubject: BehaviorSubject<boolean>;
|
||||
let premiumStatusSubject: BehaviorSubject<boolean>;
|
||||
let organizationsSubject: BehaviorSubject<Organization[]>;
|
||||
|
||||
let service: PhishingDetectionSettingsService;
|
||||
let stateProvider: FakeStateProvider;
|
||||
|
||||
// Constant mock data
|
||||
const familyOrg = mock<Organization>({
|
||||
canAccess: true,
|
||||
isMember: true,
|
||||
usersGetPremium: true,
|
||||
productTierType: ProductTierType.Families,
|
||||
usePhishingBlocker: true,
|
||||
});
|
||||
const teamOrg = mock<Organization>({
|
||||
canAccess: true,
|
||||
isMember: true,
|
||||
usersGetPremium: true,
|
||||
productTierType: ProductTierType.Teams,
|
||||
usePhishingBlocker: true,
|
||||
});
|
||||
const enterpriseOrg = mock<Organization>({
|
||||
canAccess: true,
|
||||
isMember: true,
|
||||
usersGetPremium: true,
|
||||
productTierType: ProductTierType.Enterprise,
|
||||
usePhishingBlocker: true,
|
||||
});
|
||||
|
||||
const mockUserId = "mock-user-id" as UserId;
|
||||
const account = mock<Account>({ id: mockUserId });
|
||||
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
|
||||
|
||||
beforeEach(() => {
|
||||
// Initialize subjects
|
||||
activeAccountSubject = new BehaviorSubject<Account | null>(null);
|
||||
featureFlagSubject = new BehaviorSubject<boolean>(false);
|
||||
premiumStatusSubject = new BehaviorSubject<boolean>(false);
|
||||
organizationsSubject = new BehaviorSubject<Organization[]>([]);
|
||||
|
||||
// Default implementations for required functions
|
||||
mockAccountService = mock<AccountService>();
|
||||
mockAccountService.activeAccount$ = activeAccountSubject.asObservable();
|
||||
|
||||
mockBillingService = mock<BillingAccountProfileStateService>();
|
||||
mockBillingService.hasPremiumPersonally$.mockReturnValue(premiumStatusSubject.asObservable());
|
||||
|
||||
mockConfigService = mock<ConfigService>();
|
||||
mockConfigService.getFeatureFlag$.mockReturnValue(featureFlagSubject.asObservable());
|
||||
|
||||
mockOrganizationService = mock<OrganizationService>();
|
||||
mockOrganizationService.organizations$.mockReturnValue(organizationsSubject.asObservable());
|
||||
|
||||
stateProvider = new FakeStateProvider(accountService);
|
||||
service = new PhishingDetectionSettingsService(
|
||||
mockAccountService,
|
||||
mockBillingService,
|
||||
mockConfigService,
|
||||
mockOrganizationService,
|
||||
stateProvider,
|
||||
);
|
||||
});
|
||||
|
||||
// Helper to easily get the result of the observable we are testing
|
||||
const getAccess = () => firstValueFrom(service.available$);
|
||||
|
||||
describe("enabled$", () => {
|
||||
it("should default to true if an account is logged in", async () => {
|
||||
activeAccountSubject.next(account);
|
||||
const result = await firstValueFrom(service.enabled$);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return the stored value", async () => {
|
||||
activeAccountSubject.next(account);
|
||||
|
||||
await service.setEnabled(mockUserId, false);
|
||||
const resultDisabled = await firstValueFrom(service.enabled$);
|
||||
expect(resultDisabled).toBe(false);
|
||||
|
||||
await service.setEnabled(mockUserId, true);
|
||||
const resultEnabled = await firstValueFrom(service.enabled$);
|
||||
expect(resultEnabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setEnabled", () => {
|
||||
it("should update the stored value", async () => {
|
||||
activeAccountSubject.next(account);
|
||||
await service.setEnabled(mockUserId, false);
|
||||
let result = await firstValueFrom(service.enabled$);
|
||||
expect(result).toBe(false);
|
||||
|
||||
await service.setEnabled(mockUserId, true);
|
||||
result = await firstValueFrom(service.enabled$);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("returns false immediately when the feature flag is disabled, regardless of other conditions", async () => {
|
||||
activeAccountSubject.next(account);
|
||||
premiumStatusSubject.next(true);
|
||||
organizationsSubject.next([familyOrg]);
|
||||
|
||||
featureFlagSubject.next(false);
|
||||
|
||||
await expect(getAccess()).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("returns false if there is no active account present yet", async () => {
|
||||
activeAccountSubject.next(null); // No active account
|
||||
featureFlagSubject.next(true); // Flag is on
|
||||
|
||||
await expect(getAccess()).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when feature flag is enabled and user has premium personally", async () => {
|
||||
activeAccountSubject.next(account);
|
||||
featureFlagSubject.next(true);
|
||||
organizationsSubject.next([]);
|
||||
premiumStatusSubject.next(true);
|
||||
|
||||
await expect(getAccess()).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("returns true when feature flag is enabled and user is in a Family Organization", async () => {
|
||||
activeAccountSubject.next(account);
|
||||
featureFlagSubject.next(true);
|
||||
premiumStatusSubject.next(false); // User has no personal premium
|
||||
|
||||
organizationsSubject.next([familyOrg]);
|
||||
|
||||
await expect(getAccess()).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("returns true when feature flag is enabled and user is in an Enterprise org with phishing blocker enabled", async () => {
|
||||
activeAccountSubject.next(account);
|
||||
featureFlagSubject.next(true);
|
||||
premiumStatusSubject.next(false);
|
||||
organizationsSubject.next([enterpriseOrg]);
|
||||
|
||||
await expect(getAccess()).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when user has no access through personal premium or organizations", async () => {
|
||||
activeAccountSubject.next(account);
|
||||
featureFlagSubject.next(true);
|
||||
premiumStatusSubject.next(false);
|
||||
organizationsSubject.next([teamOrg]); // Team org does not give access
|
||||
|
||||
await expect(getAccess()).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("shares/caches the available$ result between multiple subscribers", async () => {
|
||||
// Use a plain Subject for this test so we control when the premium observable emits
|
||||
// and avoid the BehaviorSubject's initial emission which can race with subscriptions.
|
||||
// Provide the Subject directly as the mock return value for the billing service
|
||||
const oneTimePremium = new Subject<boolean>();
|
||||
mockBillingService.hasPremiumPersonally$.mockReturnValueOnce(oneTimePremium.asObservable());
|
||||
|
||||
activeAccountSubject.next(account);
|
||||
featureFlagSubject.next(true);
|
||||
organizationsSubject.next([]);
|
||||
|
||||
const p1 = firstValueFrom(service.available$);
|
||||
const p2 = firstValueFrom(service.available$);
|
||||
|
||||
// Trigger the pipeline
|
||||
oneTimePremium.next(true);
|
||||
|
||||
const [first, second] = await Promise.all([p1, p2]);
|
||||
|
||||
expect(first).toBe(true);
|
||||
expect(second).toBe(true);
|
||||
// The billing function should have been called at most once due to caching
|
||||
expect(mockBillingService.hasPremiumPersonally$).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
import { combineLatest, Observable, of, switchMap } from "rxjs";
|
||||
import { catchError, distinctUntilChanged, map, shareReplay } from "rxjs/operators";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
import { PHISHING_DETECTION_DISK, StateProvider, UserKeyDefinition } from "../../../platform/state";
|
||||
import { PhishingDetectionSettingsServiceAbstraction } from "../abstractions/phishing-detection-settings.service.abstraction";
|
||||
|
||||
const ENABLE_PHISHING_DETECTION = new UserKeyDefinition(
|
||||
PHISHING_DETECTION_DISK,
|
||||
"enablePhishingDetection",
|
||||
{
|
||||
deserializer: (value: boolean) => value ?? true, // Default: enabled
|
||||
clearOn: [],
|
||||
},
|
||||
);
|
||||
|
||||
export class PhishingDetectionSettingsService implements PhishingDetectionSettingsServiceAbstraction {
|
||||
readonly available$: Observable<boolean>;
|
||||
readonly enabled$: Observable<boolean>;
|
||||
readonly on$: Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private billingService: BillingAccountProfileStateService,
|
||||
private configService: ConfigService,
|
||||
private organizationService: OrganizationService,
|
||||
private stateProvider: StateProvider,
|
||||
) {
|
||||
this.available$ = this.buildAvailablePipeline$().pipe(
|
||||
distinctUntilChanged(),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
this.enabled$ = this.buildEnabledPipeline$().pipe(
|
||||
distinctUntilChanged(),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
this.on$ = combineLatest([this.available$, this.enabled$]).pipe(
|
||||
map(([available, enabled]) => available && enabled),
|
||||
distinctUntilChanged(),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
}
|
||||
|
||||
async setEnabled(userId: UserId, enabled: boolean): Promise<void> {
|
||||
await this.stateProvider.getUser(userId, ENABLE_PHISHING_DETECTION).update(() => enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the observable pipeline to determine if phishing detection is available to the user
|
||||
*
|
||||
* @returns An observable pipeline that determines if phishing detection is available
|
||||
*/
|
||||
private buildAvailablePipeline$(): Observable<boolean> {
|
||||
return combineLatest([
|
||||
this.accountService.activeAccount$,
|
||||
this.configService.getFeatureFlag$(FeatureFlag.PhishingDetection),
|
||||
]).pipe(
|
||||
switchMap(([account, featureEnabled]) => {
|
||||
if (!account || !featureEnabled) {
|
||||
return of(false);
|
||||
}
|
||||
return combineLatest([
|
||||
this.billingService.hasPremiumPersonally$(account.id).pipe(catchError(() => of(false))),
|
||||
this.organizationService.organizations$(account.id).pipe(catchError(() => of([]))),
|
||||
]).pipe(
|
||||
map(([hasPremium, organizations]) => hasPremium || this.orgGrantsAccess(organizations)),
|
||||
catchError(() => of(false)),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the observable pipeline to determine if phishing detection is enabled by the user
|
||||
*
|
||||
* @returns True if phishing detection is enabled for the active user
|
||||
*/
|
||||
private buildEnabledPipeline$(): Observable<boolean> {
|
||||
return this.accountService.activeAccount$.pipe(
|
||||
switchMap((account) => {
|
||||
if (!account) {
|
||||
return of(false);
|
||||
}
|
||||
return this.stateProvider.getUserState$(ENABLE_PHISHING_DETECTION, account.id);
|
||||
}),
|
||||
map((enabled) => enabled ?? true),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if any of the user's organizations grant access to phishing detection
|
||||
*
|
||||
* @param organizations The organizations the user is a member of
|
||||
* @returns True if any organization grants access to phishing detection
|
||||
*/
|
||||
private orgGrantsAccess(organizations: Organization[]): boolean {
|
||||
return organizations.some((org) => {
|
||||
if (!org.canAccess || !org.isMember || !org.usersGetPremium) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
org.productTierType === ProductTierType.Families ||
|
||||
(org.productTierType === ProductTierType.Enterprise && org.usePhishingBlocker)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -10,3 +10,5 @@ export {
|
||||
VaultTimeoutNumberType,
|
||||
VaultTimeoutStringType,
|
||||
} from "./types/vault-timeout.type";
|
||||
// Only used by desktop's electron-key.service.spec.ts test
|
||||
export { VAULT_TIMEOUT } from "./services/vault-timeout-settings.state";
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
shareReplay,
|
||||
switchMap,
|
||||
tap,
|
||||
concatMap,
|
||||
} from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
@@ -150,7 +151,7 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
|
||||
return from(
|
||||
this.determineVaultTimeout(currentVaultTimeout, maxSessionTimeoutPolicyData),
|
||||
).pipe(
|
||||
tap((vaultTimeout: VaultTimeout) => {
|
||||
concatMap(async (vaultTimeout: VaultTimeout) => {
|
||||
this.logService.debug(
|
||||
"[VaultTimeoutSettingsService] Determined vault timeout is %o for user id %s",
|
||||
vaultTimeout,
|
||||
@@ -159,8 +160,9 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
|
||||
|
||||
// As a side effect, set the new value determined by determineVaultTimeout into state if it's different from the current
|
||||
if (vaultTimeout !== currentVaultTimeout) {
|
||||
return this.stateProvider.setUserState(VAULT_TIMEOUT, vaultTimeout, userId);
|
||||
await this.stateProvider.setUserState(VAULT_TIMEOUT, vaultTimeout, userId);
|
||||
}
|
||||
return vaultTimeout;
|
||||
}),
|
||||
catchError((error: unknown) => {
|
||||
// Protect outer observable from canceling on error by catching and returning EMPTY
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BehaviorSubject, from, of } from "rxjs";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { LockService, LogoutService } from "@bitwarden/auth/common";
|
||||
|
||||
import { FakeAccountService, mockAccountServiceWith } from "../../../../spec";
|
||||
import { FakeAccountService, mockAccountServiceWith, mockAccountInfoWith } from "../../../../spec";
|
||||
import { AccountInfo } from "../../../auth/abstractions/account.service";
|
||||
import { AuthService } from "../../../auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
||||
@@ -109,19 +109,19 @@ describe("VaultTimeoutService", () => {
|
||||
if (globalSetups?.userId) {
|
||||
accountService.activeAccountSubject.next({
|
||||
id: globalSetups.userId as UserId,
|
||||
email: null,
|
||||
emailVerified: false,
|
||||
name: null,
|
||||
...mockAccountInfoWith({
|
||||
email: null,
|
||||
name: null,
|
||||
}),
|
||||
});
|
||||
}
|
||||
accountService.accounts$ = of(
|
||||
Object.entries(accounts).reduce(
|
||||
(agg, [id]) => {
|
||||
agg[id] = {
|
||||
agg[id] = mockAccountInfoWith({
|
||||
email: "",
|
||||
emailVerified: true,
|
||||
name: "",
|
||||
};
|
||||
});
|
||||
return agg;
|
||||
},
|
||||
{} as Record<string, AccountInfo>,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti
|
||||
import { AuthRequestAnsweringServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
|
||||
import { mockAccountInfoWith } from "../../../../spec";
|
||||
import { AccountService } from "../../../auth/abstractions/account.service";
|
||||
import { AuthService } from "../../../auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
||||
@@ -163,9 +164,10 @@ describe("DefaultServerNotificationsService (multi-user)", () => {
|
||||
} else {
|
||||
activeUserAccount$.next({
|
||||
id: userId,
|
||||
email: "email",
|
||||
name: "Test Name",
|
||||
emailVerified: true,
|
||||
...mockAccountInfoWith({
|
||||
email: "email",
|
||||
name: "Test Name",
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -174,7 +176,10 @@ describe("DefaultServerNotificationsService (multi-user)", () => {
|
||||
const currentAccounts = (userAccounts$.getValue() as Record<string, any>) ?? {};
|
||||
userAccounts$.next({
|
||||
...currentAccounts,
|
||||
[userId]: { email: "email", name: "Test Name", emailVerified: true },
|
||||
[userId]: mockAccountInfoWith({
|
||||
email: "email",
|
||||
name: "Test Name",
|
||||
}),
|
||||
} as any);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { AuthRequestAnsweringServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction";
|
||||
|
||||
import { awaitAsync } from "../../../../spec";
|
||||
import { awaitAsync, mockAccountInfoWith } from "../../../../spec";
|
||||
import { Matrix } from "../../../../spec/matrix";
|
||||
import { AccountService } from "../../../auth/abstractions/account.service";
|
||||
import { AuthService } from "../../../auth/abstractions/auth.service";
|
||||
@@ -139,11 +139,18 @@ describe("NotificationsService", () => {
|
||||
activeAccount.next(null);
|
||||
accounts.next({} as any);
|
||||
} else {
|
||||
activeAccount.next({ id: userId, email: "email", name: "Test Name", emailVerified: true });
|
||||
const accountInfo = mockAccountInfoWith({
|
||||
email: "email",
|
||||
name: "Test Name",
|
||||
});
|
||||
activeAccount.next({
|
||||
id: userId,
|
||||
...accountInfo,
|
||||
});
|
||||
const current = (accounts.getValue() as Record<string, any>) ?? {};
|
||||
accounts.next({
|
||||
...current,
|
||||
[userId]: { email: "email", name: "Test Name", emailVerified: true },
|
||||
[userId]: accountInfo,
|
||||
} as any);
|
||||
}
|
||||
}
|
||||
@@ -349,7 +356,13 @@ describe("NotificationsService", () => {
|
||||
describe("processNotification", () => {
|
||||
beforeEach(async () => {
|
||||
appIdService.getAppId.mockResolvedValue("test-app-id");
|
||||
activeAccount.next({ id: mockUser1, email: "email", name: "Test Name", emailVerified: true });
|
||||
activeAccount.next({
|
||||
id: mockUser1,
|
||||
...mockAccountInfoWith({
|
||||
email: "email",
|
||||
name: "Test Name",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
describe("NotificationType.LogOut", () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { FakeStateProvider, awaitAsync } from "../../../spec";
|
||||
import { FakeStateProvider, awaitAsync, mockAccountInfoWith } from "../../../spec";
|
||||
import { FakeAccountService } from "../../../spec/fake-account-service";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { CloudRegion, Region } from "../abstractions/environment.service";
|
||||
@@ -28,16 +28,14 @@ describe("EnvironmentService", () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
accountService = new FakeAccountService({
|
||||
[testUser]: {
|
||||
[testUser]: mockAccountInfoWith({
|
||||
name: "name",
|
||||
email: "email",
|
||||
emailVerified: false,
|
||||
},
|
||||
[alternateTestUser]: {
|
||||
}),
|
||||
[alternateTestUser]: mockAccountInfoWith({
|
||||
name: "name",
|
||||
email: "email",
|
||||
emailVerified: false,
|
||||
},
|
||||
}),
|
||||
});
|
||||
stateProvider = new FakeStateProvider(accountService);
|
||||
|
||||
@@ -47,9 +45,10 @@ describe("EnvironmentService", () => {
|
||||
const switchUser = async (userId: UserId) => {
|
||||
accountService.activeAccountSubject.next({
|
||||
id: userId,
|
||||
email: "test@example.com",
|
||||
name: `Test Name ${userId}`,
|
||||
emailVerified: false,
|
||||
...mockAccountInfoWith({
|
||||
email: "test@example.com",
|
||||
name: `Test Name ${userId}`,
|
||||
}),
|
||||
});
|
||||
await awaitAsync();
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import { TextEncoder } from "util";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { mockAccountServiceWith } from "../../../../spec";
|
||||
import { mockAccountServiceWith, mockAccountInfoWith } from "../../../../spec";
|
||||
import { Account } from "../../../auth/abstractions/account.service";
|
||||
import { CipherId, UserId } from "../../../types/guid";
|
||||
import { CipherService, EncryptionContext } from "../../../vault/abstractions/cipher.service";
|
||||
@@ -40,9 +40,10 @@ describe("FidoAuthenticatorService", () => {
|
||||
const userId = "testId" as UserId;
|
||||
const activeAccountSubject = new BehaviorSubject<Account | null>({
|
||||
id: userId,
|
||||
email: "test@example.com",
|
||||
emailVerified: true,
|
||||
name: "Test User",
|
||||
...mockAccountInfoWith({
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
}),
|
||||
});
|
||||
|
||||
let cipherService!: MockProxy<CipherService>;
|
||||
|
||||
@@ -12,9 +12,9 @@ import {
|
||||
FakeAccountService,
|
||||
FakeStateProvider,
|
||||
mockAccountServiceWith,
|
||||
mockAccountInfoWith,
|
||||
} from "../../../../spec";
|
||||
import { ApiService } from "../../../abstractions/api.service";
|
||||
import { AccountInfo } from "../../../auth/abstractions/account.service";
|
||||
import { EncryptedString } from "../../../key-management/crypto/models/enc-string";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { UserKey } from "../../../types/key";
|
||||
@@ -92,7 +92,10 @@ describe("DefaultSdkService", () => {
|
||||
.calledWith(userId)
|
||||
.mockReturnValue(new BehaviorSubject(mock<Environment>()));
|
||||
accountService.accounts$ = of({
|
||||
[userId]: { email: "email", emailVerified: true, name: "name" } as AccountInfo,
|
||||
[userId]: mockAccountInfoWith({
|
||||
email: "email",
|
||||
name: "name",
|
||||
}),
|
||||
});
|
||||
kdfConfigService.getKdfConfig$
|
||||
.calledWith(userId)
|
||||
|
||||
@@ -8,9 +8,9 @@ import {
|
||||
FakeAccountService,
|
||||
FakeStateProvider,
|
||||
mockAccountServiceWith,
|
||||
mockAccountInfoWith,
|
||||
} from "../../../../spec";
|
||||
import { ApiService } from "../../../abstractions/api.service";
|
||||
import { AccountInfo } from "../../../auth/abstractions/account.service";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { ConfigService } from "../../abstractions/config/config.service";
|
||||
import { Environment, EnvironmentService } from "../../abstractions/environment.service";
|
||||
@@ -76,7 +76,10 @@ describe("DefaultRegisterSdkService", () => {
|
||||
.calledWith(userId)
|
||||
.mockReturnValue(new BehaviorSubject(mock<Environment>()));
|
||||
accountService.accounts$ = of({
|
||||
[userId]: { email: "email", emailVerified: true, name: "name" } as AccountInfo,
|
||||
[userId]: mockAccountInfoWith({
|
||||
email: "email",
|
||||
name: "name",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -125,7 +128,10 @@ describe("DefaultRegisterSdkService", () => {
|
||||
|
||||
it("destroys the internal SDK client when the account is removed (logout)", async () => {
|
||||
const accounts$ = new BehaviorSubject({
|
||||
[userId]: { email: "email", emailVerified: true, name: "name" } as AccountInfo,
|
||||
[userId]: mockAccountInfoWith({
|
||||
email: "email",
|
||||
name: "name",
|
||||
}),
|
||||
});
|
||||
accountService.accounts$ = accounts$;
|
||||
|
||||
|
||||
@@ -272,6 +272,7 @@ export class DefaultSyncService extends CoreSyncService {
|
||||
await this.tokenService.setSecurityStamp(response.securityStamp, response.id);
|
||||
await this.accountService.setAccountEmailVerified(response.id, response.emailVerified);
|
||||
await this.accountService.setAccountVerifyNewDeviceLogin(response.id, response.verifyDevices);
|
||||
await this.accountService.setAccountCreationDate(response.id, response.creationDate);
|
||||
|
||||
await this.billingAccountProfileStateService.setHasPremium(
|
||||
response.premiumPersonally,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ObservedValueOf, of } from "rxjs";
|
||||
import { LogoutReason } from "@bitwarden/auth/common";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
import { mockAccountInfoWith } from "../../spec";
|
||||
import { AccountService } from "../auth/abstractions/account.service";
|
||||
import { TokenService } from "../auth/abstractions/token.service";
|
||||
import { DeviceType } from "../enums";
|
||||
@@ -55,9 +56,10 @@ describe("ApiService", () => {
|
||||
|
||||
accountService.activeAccount$ = of({
|
||||
id: testActiveUser,
|
||||
email: "user1@example.com",
|
||||
emailVerified: true,
|
||||
name: "Test Name",
|
||||
...mockAccountInfoWith({
|
||||
email: "user1@example.com",
|
||||
name: "Test Name",
|
||||
}),
|
||||
} satisfies ObservedValueOf<AccountService["activeAccount$"]>);
|
||||
|
||||
httpOperations = mock();
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject, firstValueFrom } from "rxjs";
|
||||
|
||||
import { FakeAccountService, FakeStateProvider, awaitAsync } from "../../../spec";
|
||||
import {
|
||||
FakeAccountService,
|
||||
FakeStateProvider,
|
||||
awaitAsync,
|
||||
mockAccountInfoWith,
|
||||
} from "../../../spec";
|
||||
import { Account } from "../../auth/abstractions/account.service";
|
||||
import { EXTENSION_DISK, UserKeyDefinition } from "../../platform/state";
|
||||
import { UserId } from "../../types/guid";
|
||||
@@ -21,9 +26,10 @@ import { SimpleLogin } from "./vendor/simplelogin";
|
||||
const SomeUser = "some user" as UserId;
|
||||
const SomeAccount = {
|
||||
id: SomeUser,
|
||||
email: "someone@example.com",
|
||||
emailVerified: true,
|
||||
name: "Someone",
|
||||
...mockAccountInfoWith({
|
||||
email: "someone@example.com",
|
||||
name: "Someone",
|
||||
}),
|
||||
};
|
||||
const SomeAccount$ = new BehaviorSubject<Account>(SomeAccount);
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# Tools Vendor Integration
|
||||
|
||||
This module defines interfaces and helpers for creating vendor integration sites.
|
||||
|
||||
## RPC
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
FakeStateProvider,
|
||||
awaitAsync,
|
||||
mockAccountServiceWith,
|
||||
mockAccountInfoWith,
|
||||
} from "../../../../spec";
|
||||
import { KeyGenerationService } from "../../../key-management/crypto";
|
||||
import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service";
|
||||
@@ -71,9 +72,10 @@ describe("SendService", () => {
|
||||
|
||||
accountService.activeAccountSubject.next({
|
||||
id: mockUserId,
|
||||
email: "email",
|
||||
emailVerified: false,
|
||||
name: "name",
|
||||
...mockAccountInfoWith({
|
||||
email: "email",
|
||||
name: "name",
|
||||
}),
|
||||
});
|
||||
|
||||
// Initial encrypted state
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
awaitAsync,
|
||||
FakeAccountService,
|
||||
FakeStateProvider,
|
||||
mockAccountInfoWith,
|
||||
ObservableTracker,
|
||||
} from "../../../spec";
|
||||
import { Account } from "../../auth/abstractions/account.service";
|
||||
@@ -23,17 +24,19 @@ import { UserStateSubject } from "./user-state-subject";
|
||||
const SomeUser = "some user" as UserId;
|
||||
const SomeAccount = {
|
||||
id: SomeUser,
|
||||
email: "someone@example.com",
|
||||
emailVerified: true,
|
||||
name: "Someone",
|
||||
...mockAccountInfoWith({
|
||||
email: "someone@example.com",
|
||||
name: "Someone",
|
||||
}),
|
||||
};
|
||||
const SomeAccount$ = new BehaviorSubject<Account>(SomeAccount);
|
||||
|
||||
const SomeOtherAccount = {
|
||||
id: "some other user" as UserId,
|
||||
email: "someone@example.com",
|
||||
emailVerified: true,
|
||||
name: "Someone",
|
||||
...mockAccountInfoWith({
|
||||
email: "someone@example.com",
|
||||
name: "Someone",
|
||||
}),
|
||||
};
|
||||
|
||||
type TestType = { foo: string };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NgClass } from "@angular/common";
|
||||
import { Component, computed, input } from "@angular/core";
|
||||
import { ChangeDetectionStrategy, Component, computed, input } from "@angular/core";
|
||||
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
@@ -14,13 +14,11 @@ const SizeClasses: Record<SizeTypes, string[]> = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Avatars display a unique color that helps a user visually recognize their logged in account.
|
||||
|
||||
* A variance in color across the avatar component is important as it is used in Account Switching as a
|
||||
* visual indicator to recognize which of a personal or work account a user is logged into.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
* Avatars display a unique color that helps a user visually recognize their logged in account.
|
||||
*
|
||||
* A variance in color across the avatar component is important as it is used in Account Switching as a
|
||||
* visual indicator to recognize which of a personal or work account a user is logged into.
|
||||
*/
|
||||
@Component({
|
||||
selector: "bit-avatar",
|
||||
template: `
|
||||
@@ -49,13 +47,38 @@ const SizeClasses: Record<SizeTypes, string[]> = {
|
||||
</span>
|
||||
`,
|
||||
imports: [NgClass],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AvatarComponent {
|
||||
/**
|
||||
* Whether to display a border around the avatar.
|
||||
*/
|
||||
readonly border = input(false);
|
||||
|
||||
/**
|
||||
* Custom background color for the avatar. If not provided, a color will be generated based on the id or text.
|
||||
*/
|
||||
readonly color = input<string>();
|
||||
|
||||
/**
|
||||
* Unique identifier used to generate a consistent background color. Takes precedence over text for color generation.
|
||||
*/
|
||||
readonly id = input<string>();
|
||||
|
||||
/**
|
||||
* Text to display in the avatar. The first letters of words (up to 2 characters) will be shown.
|
||||
* Also used to generate background color if id is not provided.
|
||||
*/
|
||||
readonly text = input<string>();
|
||||
|
||||
/**
|
||||
* Title attribute for the avatar. If not provided, falls back to the text value.
|
||||
*/
|
||||
readonly title = input<string>();
|
||||
|
||||
/**
|
||||
* Size of the avatar.
|
||||
*/
|
||||
readonly size = input<SizeTypes>("default");
|
||||
|
||||
protected readonly svgCharCount = 2;
|
||||
|
||||
@@ -34,16 +34,21 @@
|
||||
}
|
||||
<ng-content select="[bitDialogTitle]"></ng-content>
|
||||
</h2>
|
||||
@if (!this.dialogRef?.disableClose) {
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-close"
|
||||
buttonType="main"
|
||||
size="default"
|
||||
bitDialogClose
|
||||
[label]="'close' | i18n"
|
||||
></button>
|
||||
}
|
||||
|
||||
<div class="tw-flex tw-items-center tw-justify-center">
|
||||
<ng-content select="[bitDialogHeaderEnd]"></ng-content>
|
||||
|
||||
@if (!this.dialogRef?.disableClose) {
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-close"
|
||||
buttonType="main"
|
||||
size="default"
|
||||
bitDialogClose
|
||||
[label]="'close' | i18n"
|
||||
></button>
|
||||
}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div
|
||||
|
||||
@@ -94,6 +94,7 @@ export const Default: Story = {
|
||||
<ng-container bitDialogTitle>
|
||||
<span bitBadge variant="success">Foobar</span>
|
||||
</ng-container>
|
||||
|
||||
<ng-container bitDialogContent>Dialog body text goes here.</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="button" bitButton buttonType="primary" [disabled]="loading">Save</button>
|
||||
@@ -292,3 +293,42 @@ export const WithCards: Story = {
|
||||
disableAnimations: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const HeaderEnd: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: /*html*/ `
|
||||
<bit-dialog
|
||||
[dialogSize]="dialogSize"
|
||||
[title]="title"
|
||||
[subtitle]="subtitle"
|
||||
[loading]="loading"
|
||||
[disablePadding]="disablePadding"
|
||||
[disableAnimations]="disableAnimations">
|
||||
|
||||
<ng-container bitDialogHeaderEnd>
|
||||
<span bitBadge>Archived</span>
|
||||
</ng-container>
|
||||
|
||||
<ng-container bitDialogContent>Dialog body text goes here.</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="button" bitButton buttonType="primary" [disabled]="loading">Save</button>
|
||||
<button type="button" bitButton buttonType="secondary" [disabled]="loading">Cancel</button>
|
||||
<button
|
||||
type="button"
|
||||
[disabled]="loading"
|
||||
class="tw-ms-auto"
|
||||
bitIconButton="bwi-trash"
|
||||
buttonType="danger"
|
||||
size="default"
|
||||
label="Delete"></button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
dialogSize: "small",
|
||||
title: "Very Long Title That Should Be Truncated After Two Lines To Test Header End Slot",
|
||||
subtitle: "Subtitle",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
## DIRT Card
|
||||
# DIRT Card
|
||||
|
||||
Package name: `@bitwarden/dirt-card`
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton buttonType="primary" type="submit">
|
||||
<span>{{ "importData" | i18n }}</span>
|
||||
<span>{{ "import" | i18n }}</span>
|
||||
</button>
|
||||
<button bitButton bitDialogClose buttonType="secondary" type="button">
|
||||
<span>{{ "cancel" | i18n }}</span>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
import { emptyGuid, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
@@ -41,9 +42,10 @@ describe("BitwardenPasswordProtectedImporter", () => {
|
||||
|
||||
accountService.activeAccount$ = of({
|
||||
id: emptyGuid as UserId,
|
||||
email: "test@example.com",
|
||||
emailVerified: true,
|
||||
name: "Test User",
|
||||
...mockAccountInfoWith({
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
}),
|
||||
});
|
||||
|
||||
const mockOrgId = emptyGuid as OrganizationId;
|
||||
@@ -96,9 +98,10 @@ describe("BitwardenPasswordProtectedImporter", () => {
|
||||
beforeEach(() => {
|
||||
accountService.activeAccount$ = of({
|
||||
id: emptyGuid as UserId,
|
||||
email: "test@example.com",
|
||||
emailVerified: true,
|
||||
name: "Test User",
|
||||
...mockAccountInfoWith({
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
}),
|
||||
});
|
||||
importer = new BitwardenPasswordProtectedImporter(
|
||||
keyService,
|
||||
|
||||
@@ -11,6 +11,7 @@ import { MasterPasswordUnlockService } from "@bitwarden/common/key-management/ma
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
import { UserKey } from "@bitwarden/common/types/key";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
@@ -39,9 +40,10 @@ describe("MasterPasswordLockComponent", () => {
|
||||
const mockMasterPassword = "testExample";
|
||||
const activeAccount: Account = {
|
||||
id: "user-id" as UserId,
|
||||
email: "user@example.com",
|
||||
emailVerified: true,
|
||||
name: "User",
|
||||
...mockAccountInfoWith({
|
||||
email: "user@example.com",
|
||||
name: "User",
|
||||
}),
|
||||
};
|
||||
const mockUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey;
|
||||
|
||||
|
||||
@@ -69,11 +69,13 @@ describe("keyService", () => {
|
||||
let accountService: FakeAccountService;
|
||||
let masterPasswordService: FakeMasterPasswordService;
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
accountService = mockAccountServiceWith(mockUserId);
|
||||
masterPasswordService = new FakeMasterPasswordService();
|
||||
stateProvider = new FakeStateProvider(accountService);
|
||||
|
||||
await stateProvider.setUserState(VAULT_TIMEOUT, VaultTimeoutStringType.Never, mockUserId);
|
||||
|
||||
keyService = new DefaultKeyService(
|
||||
masterPasswordService,
|
||||
keyGenerationService,
|
||||
|
||||
@@ -691,7 +691,9 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
// the VaultTimeoutSettingsSvc and this service.
|
||||
// This should be fixed as part of the PM-7082 - Auto Key Service work.
|
||||
const vaultTimeout = await firstValueFrom(
|
||||
this.stateProvider.getUserState$(VAULT_TIMEOUT, userId),
|
||||
this.stateProvider
|
||||
.getUserState$(VAULT_TIMEOUT, userId)
|
||||
.pipe(filter((timeout) => timeout != null)),
|
||||
);
|
||||
|
||||
shouldStoreKey = vaultTimeout == VaultTimeoutStringType.Never;
|
||||
|
||||
@@ -620,7 +620,7 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
title: "confirmVaultExport",
|
||||
bodyText: confirmDescription,
|
||||
confirmButtonOptions: {
|
||||
text: "exportVault",
|
||||
text: "continue",
|
||||
type: "primary",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -25,7 +25,11 @@ import { deepFreeze } from "@bitwarden/common/tools/util";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { BitwardenClient } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { FakeAccountService, FakeStateProvider } from "../../../../../common/spec";
|
||||
import {
|
||||
FakeAccountService,
|
||||
FakeStateProvider,
|
||||
mockAccountInfoWith,
|
||||
} from "../../../../../common/spec";
|
||||
import { Algorithm, AlgorithmsByType, CredentialAlgorithm, Type, Types } from "../metadata";
|
||||
import catchall from "../metadata/email/catchall";
|
||||
import plusAddress from "../metadata/email/plus-address";
|
||||
@@ -40,9 +44,10 @@ import { GeneratorMetadataProvider } from "./generator-metadata-provider";
|
||||
const SomeUser = "some user" as UserId;
|
||||
const SomeAccount = {
|
||||
id: SomeUser,
|
||||
email: "someone@example.com",
|
||||
emailVerified: true,
|
||||
name: "Someone",
|
||||
...mockAccountInfoWith({
|
||||
email: "someone@example.com",
|
||||
name: "Someone",
|
||||
}),
|
||||
};
|
||||
const SomeAccount$ = new BehaviorSubject<Account>(SomeAccount);
|
||||
|
||||
|
||||
@@ -15,7 +15,12 @@ import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/stat
|
||||
import { StateConstraints } from "@bitwarden/common/tools/types";
|
||||
import { OrganizationId, PolicyId, UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { FakeStateProvider, FakeAccountService, awaitAsync } from "../../../../../common/spec";
|
||||
import {
|
||||
FakeStateProvider,
|
||||
FakeAccountService,
|
||||
awaitAsync,
|
||||
mockAccountInfoWith,
|
||||
} from "../../../../../common/spec";
|
||||
import { CoreProfileMetadata, ProfileContext } from "../metadata/profile-metadata";
|
||||
import { GeneratorConstraints } from "../types";
|
||||
|
||||
@@ -31,21 +36,25 @@ const UnverifiedEmailUser = "UnverifiedEmailUser" as UserId;
|
||||
const accounts: Record<UserId, Account> = {
|
||||
[SomeUser]: {
|
||||
id: SomeUser,
|
||||
name: "some user",
|
||||
email: "some.user@example.com",
|
||||
emailVerified: true,
|
||||
...mockAccountInfoWith({
|
||||
name: "some user",
|
||||
email: "some.user@example.com",
|
||||
}),
|
||||
},
|
||||
[AnotherUser]: {
|
||||
id: AnotherUser,
|
||||
name: "some other user",
|
||||
email: "some.other.user@example.com",
|
||||
emailVerified: true,
|
||||
...mockAccountInfoWith({
|
||||
name: "some other user",
|
||||
email: "some.other.user@example.com",
|
||||
}),
|
||||
},
|
||||
[UnverifiedEmailUser]: {
|
||||
id: UnverifiedEmailUser,
|
||||
name: "a user with an unverfied email",
|
||||
email: "unverified@example.com",
|
||||
emailVerified: false,
|
||||
...mockAccountInfoWith({
|
||||
name: "a user with an unverfied email",
|
||||
email: "unverified@example.com",
|
||||
emailVerified: false,
|
||||
}),
|
||||
},
|
||||
};
|
||||
const accountService = new FakeAccountService(accounts);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Vendor } from "@bitwarden/common/tools/extension/vendor/data";
|
||||
import { SemanticLogger, ifEnabledSemanticLoggerProvider } from "@bitwarden/common/tools/log";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { awaitAsync } from "../../../../../common/spec";
|
||||
import { awaitAsync, mockAccountInfoWith } from "../../../../../common/spec";
|
||||
import {
|
||||
Algorithm,
|
||||
CredentialAlgorithm,
|
||||
@@ -56,9 +56,10 @@ describe("DefaultCredentialGeneratorService", () => {
|
||||
// Use a hard-coded value for mockAccount
|
||||
account = {
|
||||
id: "test-account-id" as UserId,
|
||||
emailVerified: true,
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
...mockAccountInfoWith({
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
}),
|
||||
};
|
||||
|
||||
system = {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ChipSelectComponent } from "@bitwarden/components";
|
||||
|
||||
@@ -31,9 +32,11 @@ describe("SendListFiltersComponent", () => {
|
||||
|
||||
accountService.activeAccount$ = of({
|
||||
id: userId,
|
||||
email: "test@email.com",
|
||||
emailVerified: true,
|
||||
name: "Test User",
|
||||
...mockAccountInfoWith({
|
||||
email: "test@email.com",
|
||||
name: "Test User",
|
||||
emailVerified: true,
|
||||
}),
|
||||
});
|
||||
billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true));
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { EventType } from "@bitwarden/common/enums";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
@@ -34,9 +35,10 @@ describe("LoginCredentialsViewComponent", () => {
|
||||
const hasPremiumFromAnySource$ = new BehaviorSubject<boolean>(true);
|
||||
const mockAccount = {
|
||||
id: "test-user-id" as UserId,
|
||||
email: "test@example.com",
|
||||
emailVerified: true,
|
||||
name: "Test User",
|
||||
...mockAccountInfoWith({
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
}),
|
||||
type: 0,
|
||||
status: 0,
|
||||
kdf: 0,
|
||||
|
||||
@@ -2,9 +2,10 @@ import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
@@ -47,11 +48,7 @@ describe("AddEditFolderDialogComponent", () => {
|
||||
showToast.mockClear();
|
||||
|
||||
const userId = "" as UserId;
|
||||
const accountInfo: AccountInfo = {
|
||||
email: "",
|
||||
emailVerified: true,
|
||||
name: undefined,
|
||||
};
|
||||
const accountInfo = mockAccountInfoWith();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AddEditFolderDialogComponent, NoopAnimationsModule],
|
||||
|
||||
Reference in New Issue
Block a user