mirror of
https://github.com/bitwarden/browser.git
synced 2024-12-22 16:29:09 +01:00
[PM-7169][PM-5267] Remove auth status from account info (#8539)
* remove active account unlocked from state service * Remove status from account service `AccountInfo` * Fixup lingering usages of status Fixup missed factories * Fixup account info usage * fixup CLI build * Fixup current account type * Add helper for all auth statuses to auth service * Fix tests * Uncomment mistakenly commented code * Rework logged out account exclusion tests * Correct test description * Avoid getters returning observables * fixup type
This commit is contained in:
parent
c7ea35280d
commit
8d698d9d84
@ -6,6 +6,7 @@ import { Subject, firstValueFrom, map, switchMap, takeUntil } from "rxjs";
|
|||||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
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 { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
|
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
@ -32,6 +33,7 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
|||||||
private location: Location,
|
private location: Location,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||||
|
private authService: AuthService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
get accountLimit() {
|
get accountLimit() {
|
||||||
@ -42,13 +44,14 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
|||||||
return this.accountSwitcherService.SPECIAL_ADD_ACCOUNT_ID;
|
return this.accountSwitcherService.SPECIAL_ADD_ACCOUNT_ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
get availableAccounts$() {
|
readonly availableAccounts$ = this.accountSwitcherService.availableAccounts$;
|
||||||
return this.accountSwitcherService.availableAccounts$;
|
readonly currentAccount$ = this.accountService.activeAccount$.pipe(
|
||||||
}
|
switchMap((a) =>
|
||||||
|
a == null
|
||||||
get currentAccount$() {
|
? null
|
||||||
return this.accountService.activeAccount$;
|
: this.authService.activeAccountStatus$.pipe(map((s) => ({ ...a, status: s }))),
|
||||||
}
|
),
|
||||||
|
);
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
const availableVaultTimeoutActions = await firstValueFrom(
|
const availableVaultTimeoutActions = await firstValueFrom(
|
||||||
|
@ -4,6 +4,7 @@ import { ActivatedRoute, Router } from "@angular/router";
|
|||||||
import { Observable, combineLatest, switchMap } from "rxjs";
|
import { Observable, combineLatest, switchMap } from "rxjs";
|
||||||
|
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
|
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
@ -29,12 +30,14 @@ export class CurrentAccountComponent {
|
|||||||
private router: Router,
|
private router: Router,
|
||||||
private location: Location,
|
private location: Location,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
|
private authService: AuthService,
|
||||||
) {
|
) {
|
||||||
this.currentAccount$ = combineLatest([
|
this.currentAccount$ = combineLatest([
|
||||||
this.accountService.activeAccount$,
|
this.accountService.activeAccount$,
|
||||||
this.avatarService.avatarColor$,
|
this.avatarService.avatarColor$,
|
||||||
|
this.authService.activeAccountStatus$,
|
||||||
]).pipe(
|
]).pipe(
|
||||||
switchMap(async ([account, avatarColor]) => {
|
switchMap(async ([account, avatarColor, accountStatus]) => {
|
||||||
if (account == null) {
|
if (account == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -42,7 +45,7 @@ export class CurrentAccountComponent {
|
|||||||
id: account.id,
|
id: account.id,
|
||||||
name: account.name || account.email,
|
name: account.name || account.email,
|
||||||
email: account.email,
|
email: account.email,
|
||||||
status: account.status,
|
status: accountStatus,
|
||||||
avatarColor,
|
avatarColor,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { matches, mock } from "jest-mock-extended";
|
import { matches, mock } from "jest-mock-extended";
|
||||||
import { BehaviorSubject, firstValueFrom, of, timeout } from "rxjs";
|
import { BehaviorSubject, ReplaySubject, firstValueFrom, of, timeout } from "rxjs";
|
||||||
|
|
||||||
import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
|
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
@ -12,22 +13,29 @@ import { UserId } from "@bitwarden/common/types/guid";
|
|||||||
import { AccountSwitcherService } from "./account-switcher.service";
|
import { AccountSwitcherService } from "./account-switcher.service";
|
||||||
|
|
||||||
describe("AccountSwitcherService", () => {
|
describe("AccountSwitcherService", () => {
|
||||||
const accountsSubject = new BehaviorSubject<Record<UserId, AccountInfo>>(null);
|
let accountsSubject: BehaviorSubject<Record<UserId, AccountInfo>>;
|
||||||
const activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>(null);
|
let activeAccountSubject: BehaviorSubject<{ id: UserId } & AccountInfo>;
|
||||||
|
let authStatusSubject: ReplaySubject<Record<UserId, AuthenticationStatus>>;
|
||||||
|
|
||||||
const accountService = mock<AccountService>();
|
const accountService = mock<AccountService>();
|
||||||
const avatarService = mock<AvatarService>();
|
const avatarService = mock<AvatarService>();
|
||||||
const messagingService = mock<MessagingService>();
|
const messagingService = mock<MessagingService>();
|
||||||
const environmentService = mock<EnvironmentService>();
|
const environmentService = mock<EnvironmentService>();
|
||||||
const logService = mock<LogService>();
|
const logService = mock<LogService>();
|
||||||
|
const authService = mock<AuthService>();
|
||||||
|
|
||||||
let accountSwitcherService: AccountSwitcherService;
|
let accountSwitcherService: AccountSwitcherService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
|
accountsSubject = new BehaviorSubject<Record<UserId, AccountInfo>>(null);
|
||||||
|
activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>(null);
|
||||||
|
authStatusSubject = new ReplaySubject<Record<UserId, AuthenticationStatus>>(1);
|
||||||
|
|
||||||
|
// Use subject to allow for easy updates
|
||||||
accountService.accounts$ = accountsSubject;
|
accountService.accounts$ = accountsSubject;
|
||||||
accountService.activeAccount$ = activeAccountSubject;
|
accountService.activeAccount$ = activeAccountSubject;
|
||||||
|
authService.authStatuses$ = authStatusSubject;
|
||||||
|
|
||||||
accountSwitcherService = new AccountSwitcherService(
|
accountSwitcherService = new AccountSwitcherService(
|
||||||
accountService,
|
accountService,
|
||||||
@ -35,48 +43,59 @@ describe("AccountSwitcherService", () => {
|
|||||||
messagingService,
|
messagingService,
|
||||||
environmentService,
|
environmentService,
|
||||||
logService,
|
logService,
|
||||||
|
authService,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
accountsSubject.complete();
|
||||||
|
activeAccountSubject.complete();
|
||||||
|
authStatusSubject.complete();
|
||||||
|
});
|
||||||
|
|
||||||
describe("availableAccounts$", () => {
|
describe("availableAccounts$", () => {
|
||||||
it("should return all accounts and an add account option when accounts are less than 5", async () => {
|
it("should return all logged in accounts and an add account option when accounts are less than 5", async () => {
|
||||||
const user1AccountInfo: AccountInfo = {
|
const accountInfo: AccountInfo = {
|
||||||
name: "Test User 1",
|
name: "Test User 1",
|
||||||
email: "test1@email.com",
|
email: "test1@email.com",
|
||||||
status: AuthenticationStatus.Unlocked,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
avatarService.getUserAvatarColor$.mockReturnValue(of("#cccccc"));
|
avatarService.getUserAvatarColor$.mockReturnValue(of("#cccccc"));
|
||||||
accountsSubject.next({
|
accountsSubject.next({ ["1" as UserId]: accountInfo, ["2" as UserId]: accountInfo });
|
||||||
"1": user1AccountInfo,
|
authStatusSubject.next({
|
||||||
} as Record<UserId, AccountInfo>);
|
["1" as UserId]: AuthenticationStatus.Unlocked,
|
||||||
|
["2" as UserId]: AuthenticationStatus.Locked,
|
||||||
activeAccountSubject.next(Object.assign(user1AccountInfo, { id: "1" as UserId }));
|
});
|
||||||
|
activeAccountSubject.next(Object.assign(accountInfo, { id: "1" as UserId }));
|
||||||
|
|
||||||
const accounts = await firstValueFrom(
|
const accounts = await firstValueFrom(
|
||||||
accountSwitcherService.availableAccounts$.pipe(timeout(20)),
|
accountSwitcherService.availableAccounts$.pipe(timeout(20)),
|
||||||
);
|
);
|
||||||
expect(accounts).toHaveLength(2);
|
expect(accounts).toHaveLength(3);
|
||||||
expect(accounts[0].id).toBe("1");
|
expect(accounts[0].id).toBe("1");
|
||||||
expect(accounts[0].isActive).toBeTruthy();
|
expect(accounts[0].isActive).toBeTruthy();
|
||||||
|
expect(accounts[1].id).toBe("2");
|
||||||
expect(accounts[1].id).toBe("addAccount");
|
|
||||||
expect(accounts[1].isActive).toBeFalsy();
|
expect(accounts[1].isActive).toBeFalsy();
|
||||||
|
|
||||||
|
expect(accounts[2].id).toBe("addAccount");
|
||||||
|
expect(accounts[2].isActive).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([5, 6])(
|
it.each([5, 6])(
|
||||||
"should return only accounts if there are %i accounts",
|
"should return only accounts if there are %i accounts",
|
||||||
async (numberOfAccounts) => {
|
async (numberOfAccounts) => {
|
||||||
const seedAccounts: Record<UserId, AccountInfo> = {};
|
const seedAccounts: Record<UserId, AccountInfo> = {};
|
||||||
|
const seedStatuses: Record<UserId, AuthenticationStatus> = {};
|
||||||
for (let i = 0; i < numberOfAccounts; i++) {
|
for (let i = 0; i < numberOfAccounts; i++) {
|
||||||
seedAccounts[`${i}` as UserId] = {
|
seedAccounts[`${i}` as UserId] = {
|
||||||
email: `test${i}@email.com`,
|
email: `test${i}@email.com`,
|
||||||
name: "Test User ${i}",
|
name: "Test User ${i}",
|
||||||
status: AuthenticationStatus.Unlocked,
|
|
||||||
};
|
};
|
||||||
|
seedStatuses[`${i}` as UserId] = AuthenticationStatus.Unlocked;
|
||||||
}
|
}
|
||||||
avatarService.getUserAvatarColor$.mockReturnValue(of("#cccccc"));
|
avatarService.getUserAvatarColor$.mockReturnValue(of("#cccccc"));
|
||||||
accountsSubject.next(seedAccounts);
|
accountsSubject.next(seedAccounts);
|
||||||
|
authStatusSubject.next(seedStatuses);
|
||||||
activeAccountSubject.next(
|
activeAccountSubject.next(
|
||||||
Object.assign(seedAccounts["1" as UserId], { id: "1" as UserId }),
|
Object.assign(seedAccounts["1" as UserId], { id: "1" as UserId }),
|
||||||
);
|
);
|
||||||
@ -89,6 +108,26 @@ describe("AccountSwitcherService", () => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
it("excludes logged out accounts", async () => {
|
||||||
|
const user1AccountInfo: AccountInfo = {
|
||||||
|
name: "Test User 1",
|
||||||
|
email: "",
|
||||||
|
};
|
||||||
|
accountsSubject.next({ ["1" as UserId]: user1AccountInfo });
|
||||||
|
authStatusSubject.next({ ["1" as UserId]: AuthenticationStatus.LoggedOut });
|
||||||
|
accountsSubject.next({
|
||||||
|
"1": user1AccountInfo,
|
||||||
|
} as Record<UserId, AccountInfo>);
|
||||||
|
|
||||||
|
const accounts = await firstValueFrom(
|
||||||
|
accountSwitcherService.availableAccounts$.pipe(timeout(20)),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add account only
|
||||||
|
expect(accounts).toHaveLength(1);
|
||||||
|
expect(accounts[0].id).toBe("addAccount");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("selectAccount", () => {
|
describe("selectAccount", () => {
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
|
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
|
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
@ -48,25 +49,27 @@ export class AccountSwitcherService {
|
|||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
private environmentService: EnvironmentService,
|
private environmentService: EnvironmentService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
|
authService: AuthService,
|
||||||
) {
|
) {
|
||||||
this.availableAccounts$ = combineLatest([
|
this.availableAccounts$ = combineLatest([
|
||||||
this.accountService.accounts$,
|
accountService.accounts$,
|
||||||
|
authService.authStatuses$,
|
||||||
this.accountService.activeAccount$,
|
this.accountService.activeAccount$,
|
||||||
]).pipe(
|
]).pipe(
|
||||||
switchMap(async ([accounts, activeAccount]) => {
|
switchMap(async ([accounts, accountStatuses, activeAccount]) => {
|
||||||
const accountEntries = Object.entries(accounts).filter(
|
const loggedInIds = Object.keys(accounts).filter(
|
||||||
([_, account]) => account.status !== AuthenticationStatus.LoggedOut,
|
(id: UserId) => accountStatuses[id] !== AuthenticationStatus.LoggedOut,
|
||||||
);
|
);
|
||||||
// Accounts shouldn't ever be more than ACCOUNT_LIMIT but just in case do a greater than
|
// Accounts shouldn't ever be more than ACCOUNT_LIMIT but just in case do a greater than
|
||||||
const hasMaxAccounts = accountEntries.length >= this.ACCOUNT_LIMIT;
|
const hasMaxAccounts = loggedInIds.length >= this.ACCOUNT_LIMIT;
|
||||||
const options: AvailableAccount[] = await Promise.all(
|
const options: AvailableAccount[] = await Promise.all(
|
||||||
accountEntries.map(async ([id, account]) => {
|
loggedInIds.map(async (id: UserId) => {
|
||||||
return {
|
return {
|
||||||
name: account.name ?? account.email,
|
name: accounts[id].name ?? accounts[id].email,
|
||||||
email: account.email,
|
email: accounts[id].email,
|
||||||
id: id,
|
id: id,
|
||||||
server: (await this.environmentService.getEnvironment(id))?.getHostname(),
|
server: (await this.environmentService.getEnvironment(id))?.getHostname(),
|
||||||
status: account.status,
|
status: accountStatuses[id],
|
||||||
isActive: id === activeAccount?.id,
|
isActive: id === activeAccount?.id,
|
||||||
avatarColor: await firstValueFrom(
|
avatarColor: await firstValueFrom(
|
||||||
this.avatarService.getUserAvatarColor$(id as UserId),
|
this.avatarService.getUserAvatarColor$(id as UserId),
|
||||||
|
@ -779,14 +779,14 @@ export default class MainBackground {
|
|||||||
this.apiService,
|
this.apiService,
|
||||||
this.stateProvider,
|
this.stateProvider,
|
||||||
this.logService,
|
this.logService,
|
||||||
this.accountService,
|
this.authService,
|
||||||
);
|
);
|
||||||
this.eventCollectionService = new EventCollectionService(
|
this.eventCollectionService = new EventCollectionService(
|
||||||
this.cipherService,
|
this.cipherService,
|
||||||
this.stateProvider,
|
this.stateProvider,
|
||||||
this.organizationService,
|
this.organizationService,
|
||||||
this.eventUploadService,
|
this.eventUploadService,
|
||||||
this.accountService,
|
this.authService,
|
||||||
);
|
);
|
||||||
this.totpService = new TotpService(this.cryptoFunctionService, this.logService);
|
this.totpService = new TotpService(this.cryptoFunctionService, this.logService);
|
||||||
|
|
||||||
|
@ -5,7 +5,10 @@ import {
|
|||||||
organizationServiceFactory,
|
organizationServiceFactory,
|
||||||
OrganizationServiceInitOptions,
|
OrganizationServiceInitOptions,
|
||||||
} from "../../admin-console/background/service-factories/organization-service.factory";
|
} from "../../admin-console/background/service-factories/organization-service.factory";
|
||||||
import { accountServiceFactory } from "../../auth/background/service-factories/account-service.factory";
|
import {
|
||||||
|
authServiceFactory,
|
||||||
|
AuthServiceInitOptions,
|
||||||
|
} from "../../auth/background/service-factories/auth-service.factory";
|
||||||
import {
|
import {
|
||||||
FactoryOptions,
|
FactoryOptions,
|
||||||
CachedServices,
|
CachedServices,
|
||||||
@ -29,7 +32,8 @@ export type EventCollectionServiceInitOptions = EventCollectionServiceOptions &
|
|||||||
CipherServiceInitOptions &
|
CipherServiceInitOptions &
|
||||||
StateServiceInitOptions &
|
StateServiceInitOptions &
|
||||||
OrganizationServiceInitOptions &
|
OrganizationServiceInitOptions &
|
||||||
EventUploadServiceInitOptions;
|
EventUploadServiceInitOptions &
|
||||||
|
AuthServiceInitOptions;
|
||||||
|
|
||||||
export function eventCollectionServiceFactory(
|
export function eventCollectionServiceFactory(
|
||||||
cache: { eventCollectionService?: AbstractEventCollectionService } & CachedServices,
|
cache: { eventCollectionService?: AbstractEventCollectionService } & CachedServices,
|
||||||
@ -45,7 +49,7 @@ export function eventCollectionServiceFactory(
|
|||||||
await stateProviderFactory(cache, opts),
|
await stateProviderFactory(cache, opts),
|
||||||
await organizationServiceFactory(cache, opts),
|
await organizationServiceFactory(cache, opts),
|
||||||
await eventUploadServiceFactory(cache, opts),
|
await eventUploadServiceFactory(cache, opts),
|
||||||
await accountServiceFactory(cache, opts),
|
await authServiceFactory(cache, opts),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import { EventUploadService as AbstractEventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
|
import { EventUploadService as AbstractEventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
|
||||||
import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service";
|
import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service";
|
||||||
|
|
||||||
import { accountServiceFactory } from "../../auth/background/service-factories/account-service.factory";
|
import {
|
||||||
|
AuthServiceInitOptions,
|
||||||
|
authServiceFactory,
|
||||||
|
} from "../../auth/background/service-factories/auth-service.factory";
|
||||||
import {
|
import {
|
||||||
ApiServiceInitOptions,
|
ApiServiceInitOptions,
|
||||||
apiServiceFactory,
|
apiServiceFactory,
|
||||||
@ -23,7 +26,8 @@ type EventUploadServiceOptions = FactoryOptions;
|
|||||||
export type EventUploadServiceInitOptions = EventUploadServiceOptions &
|
export type EventUploadServiceInitOptions = EventUploadServiceOptions &
|
||||||
ApiServiceInitOptions &
|
ApiServiceInitOptions &
|
||||||
StateServiceInitOptions &
|
StateServiceInitOptions &
|
||||||
LogServiceInitOptions;
|
LogServiceInitOptions &
|
||||||
|
AuthServiceInitOptions;
|
||||||
|
|
||||||
export function eventUploadServiceFactory(
|
export function eventUploadServiceFactory(
|
||||||
cache: { eventUploadService?: AbstractEventUploadService } & CachedServices,
|
cache: { eventUploadService?: AbstractEventUploadService } & CachedServices,
|
||||||
@ -38,7 +42,7 @@ export function eventUploadServiceFactory(
|
|||||||
await apiServiceFactory(cache, opts),
|
await apiServiceFactory(cache, opts),
|
||||||
await stateProviderFactory(cache, opts),
|
await stateProviderFactory(cache, opts),
|
||||||
await logServiceFactory(cache, opts),
|
await logServiceFactory(cache, opts),
|
||||||
await accountServiceFactory(cache, opts),
|
await authServiceFactory(cache, opts),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { Component, Input } from "@angular/core";
|
import { Component, Input } from "@angular/core";
|
||||||
import { Observable, map } from "rxjs";
|
import { Observable, combineLatest, map, of, switchMap } from "rxjs";
|
||||||
|
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
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 { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
import { enableAccountSwitching } from "../flags";
|
import { enableAccountSwitching } from "../flags";
|
||||||
|
|
||||||
@ -14,14 +16,18 @@ export class HeaderComponent {
|
|||||||
@Input() noTheme = false;
|
@Input() noTheme = false;
|
||||||
@Input() hideAccountSwitcher = false;
|
@Input() hideAccountSwitcher = false;
|
||||||
authedAccounts$: Observable<boolean>;
|
authedAccounts$: Observable<boolean>;
|
||||||
constructor(accountService: AccountService) {
|
constructor(accountService: AccountService, authService: AuthService) {
|
||||||
this.authedAccounts$ = accountService.accounts$.pipe(
|
this.authedAccounts$ = accountService.accounts$.pipe(
|
||||||
map((accounts) => {
|
switchMap((accounts) => {
|
||||||
if (!enableAccountSwitching()) {
|
if (!enableAccountSwitching()) {
|
||||||
return false;
|
return of(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.values(accounts).some((a) => a.status !== AuthenticationStatus.LoggedOut);
|
return combineLatest(
|
||||||
|
Object.keys(accounts).map((id) => authService.authStatusFor$(id as UserId)),
|
||||||
|
).pipe(
|
||||||
|
map((statuses) => statuses.some((status) => status !== AuthenticationStatus.LoggedOut)),
|
||||||
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { NavigationEnd, Router, RouterOutlet } from "@angular/router";
|
import { NavigationEnd, Router, RouterOutlet } from "@angular/router";
|
||||||
import { ToastrService } from "ngx-toastr";
|
import { ToastrService } from "ngx-toastr";
|
||||||
import { filter, concatMap, Subject, takeUntil, firstValueFrom } from "rxjs";
|
import { filter, concatMap, Subject, takeUntil, firstValueFrom, map } from "rxjs";
|
||||||
|
|
||||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { DialogService, SimpleDialogOptions } from "@bitwarden/components";
|
import { DialogService, SimpleDialogOptions } from "@bitwarden/components";
|
||||||
@ -57,8 +58,9 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
this.activeUserId = userId;
|
this.activeUserId = userId;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.stateService.activeAccountUnlocked$
|
this.authService.activeAccountStatus$
|
||||||
.pipe(
|
.pipe(
|
||||||
|
map((status) => status === AuthenticationStatus.Unlocked),
|
||||||
filter((unlocked) => unlocked),
|
filter((unlocked) => unlocked),
|
||||||
concatMap(async () => {
|
concatMap(async () => {
|
||||||
await this.recordActivity();
|
await this.recordActivity();
|
||||||
|
@ -678,7 +678,7 @@ export class Main {
|
|||||||
this.apiService,
|
this.apiService,
|
||||||
this.stateProvider,
|
this.stateProvider,
|
||||||
this.logService,
|
this.logService,
|
||||||
this.accountService,
|
this.authService,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.eventCollectionService = new EventCollectionService(
|
this.eventCollectionService = new EventCollectionService(
|
||||||
@ -686,7 +686,7 @@ export class Main {
|
|||||||
this.stateProvider,
|
this.stateProvider,
|
||||||
this.organizationService,
|
this.organizationService,
|
||||||
this.eventUploadService,
|
this.eventUploadService,
|
||||||
this.accountService,
|
this.authService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@ import { Injectable, inject } from "@angular/core";
|
|||||||
import { CanActivate, CanActivateFn, Router, UrlTree } from "@angular/router";
|
import { CanActivate, CanActivateFn, Router, UrlTree } from "@angular/router";
|
||||||
import { Observable, map } from "rxjs";
|
import { Observable, map } from "rxjs";
|
||||||
|
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
|
||||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
|
|
||||||
@ -43,14 +42,14 @@ const defaultRoutes: UnauthRoutes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function unauthGuard(routes: UnauthRoutes): Observable<boolean | UrlTree> {
|
function unauthGuard(routes: UnauthRoutes): Observable<boolean | UrlTree> {
|
||||||
const accountService = inject(AccountService);
|
const authService = inject(AuthService);
|
||||||
const router = inject(Router);
|
const router = inject(Router);
|
||||||
|
|
||||||
return accountService.activeAccount$.pipe(
|
return authService.activeAccountStatus$.pipe(
|
||||||
map((accountData) => {
|
map((status) => {
|
||||||
if (accountData == null || accountData.status === AuthenticationStatus.LoggedOut) {
|
if (status == null || status === AuthenticationStatus.LoggedOut) {
|
||||||
return true;
|
return true;
|
||||||
} else if (accountData.status === AuthenticationStatus.Locked) {
|
} else if (status === AuthenticationStatus.Locked) {
|
||||||
return router.createUrlTree([routes.locked]);
|
return router.createUrlTree([routes.locked]);
|
||||||
} else {
|
} else {
|
||||||
return router.createUrlTree([routes.homepage()]);
|
return router.createUrlTree([routes.homepage()]);
|
||||||
|
@ -756,7 +756,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
safeProvider({
|
safeProvider({
|
||||||
provide: EventUploadServiceAbstraction,
|
provide: EventUploadServiceAbstraction,
|
||||||
useClass: EventUploadService,
|
useClass: EventUploadService,
|
||||||
deps: [ApiServiceAbstraction, StateProvider, LogService, AccountServiceAbstraction],
|
deps: [ApiServiceAbstraction, StateProvider, LogService, AuthServiceAbstraction],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: EventCollectionServiceAbstraction,
|
provide: EventCollectionServiceAbstraction,
|
||||||
@ -766,7 +766,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
StateProvider,
|
StateProvider,
|
||||||
OrganizationServiceAbstraction,
|
OrganizationServiceAbstraction,
|
||||||
EventUploadServiceAbstraction,
|
EventUploadServiceAbstraction,
|
||||||
AccountServiceAbstraction,
|
AuthServiceAbstraction,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import {
|
import {
|
||||||
FakeAccountService,
|
FakeAccountService,
|
||||||
@ -66,7 +65,6 @@ describe("UserDecryptionOptionsService", () => {
|
|||||||
await fakeAccountService.addAccount(givenUser, {
|
await fakeAccountService.addAccount(givenUser, {
|
||||||
name: "Test User 1",
|
name: "Test User 1",
|
||||||
email: "test1@email.com",
|
email: "test1@email.com",
|
||||||
status: AuthenticationStatus.Locked,
|
|
||||||
});
|
});
|
||||||
await fakeStateProvider.setUserState(
|
await fakeStateProvider.setUserState(
|
||||||
USER_DECRYPTION_OPTIONS,
|
USER_DECRYPTION_OPTIONS,
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
import { Observable, ReplaySubject } from "rxjs";
|
import { ReplaySubject } from "rxjs";
|
||||||
|
|
||||||
import { AccountInfo, AccountService } from "../src/auth/abstractions/account.service";
|
import { AccountInfo, AccountService } from "../src/auth/abstractions/account.service";
|
||||||
import { AuthenticationStatus } from "../src/auth/enums/authentication-status";
|
|
||||||
import { UserId } from "../src/types/guid";
|
import { UserId } from "../src/types/guid";
|
||||||
|
|
||||||
export function mockAccountServiceWith(
|
export function mockAccountServiceWith(
|
||||||
@ -14,7 +13,6 @@ export function mockAccountServiceWith(
|
|||||||
...{
|
...{
|
||||||
name: "name",
|
name: "name",
|
||||||
email: "email",
|
email: "email",
|
||||||
status: AuthenticationStatus.Locked,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const service = new FakeAccountService({ [userId]: fullInfo });
|
const service = new FakeAccountService({ [userId]: fullInfo });
|
||||||
@ -34,8 +32,6 @@ export class FakeAccountService implements AccountService {
|
|||||||
}
|
}
|
||||||
accounts$ = this.accountsSubject.asObservable();
|
accounts$ = this.accountsSubject.asObservable();
|
||||||
activeAccount$ = this.activeAccountSubject.asObservable();
|
activeAccount$ = this.activeAccountSubject.asObservable();
|
||||||
accountLock$: Observable<UserId>;
|
|
||||||
accountLogout$: Observable<UserId>;
|
|
||||||
|
|
||||||
constructor(initialData: Record<UserId, AccountInfo>) {
|
constructor(initialData: Record<UserId, AccountInfo>) {
|
||||||
this.accountsSubject.next(initialData);
|
this.accountsSubject.next(initialData);
|
||||||
@ -57,14 +53,6 @@ export class FakeAccountService implements AccountService {
|
|||||||
await this.mock.setAccountEmail(userId, email);
|
await this.mock.setAccountEmail(userId, email);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setAccountStatus(userId: UserId, status: AuthenticationStatus): Promise<void> {
|
|
||||||
await this.mock.setAccountStatus(userId, status);
|
|
||||||
}
|
|
||||||
|
|
||||||
async setMaxAccountStatus(userId: UserId, maxStatus: AuthenticationStatus): Promise<void> {
|
|
||||||
await this.mock.setMaxAccountStatus(userId, maxStatus);
|
|
||||||
}
|
|
||||||
|
|
||||||
async switchAccount(userId: UserId): Promise<void> {
|
async switchAccount(userId: UserId): Promise<void> {
|
||||||
const next =
|
const next =
|
||||||
userId == null ? null : { id: userId, ...this.accountsSubject["_buffer"]?.[0]?.[userId] };
|
userId == null ? null : { id: userId, ...this.accountsSubject["_buffer"]?.[0]?.[userId] };
|
||||||
|
@ -1,27 +1,23 @@
|
|||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
import { UserId } from "../../types/guid";
|
import { UserId } from "../../types/guid";
|
||||||
import { AuthenticationStatus } from "../enums/authentication-status";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holds information about an account for use in the AccountService
|
* Holds information about an account for use in the AccountService
|
||||||
* if more information is added, be sure to update the equality method.
|
* if more information is added, be sure to update the equality method.
|
||||||
*/
|
*/
|
||||||
export type AccountInfo = {
|
export type AccountInfo = {
|
||||||
status: AuthenticationStatus;
|
|
||||||
email: string;
|
email: string;
|
||||||
name: string | undefined;
|
name: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function accountInfoEqual(a: AccountInfo, b: AccountInfo) {
|
export function accountInfoEqual(a: AccountInfo, b: AccountInfo) {
|
||||||
return a?.status === b?.status && a?.email === b?.email && a?.name === b?.name;
|
return a?.email === b?.email && a?.name === b?.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class AccountService {
|
export abstract class AccountService {
|
||||||
accounts$: Observable<Record<UserId, AccountInfo>>;
|
accounts$: Observable<Record<UserId, AccountInfo>>;
|
||||||
activeAccount$: Observable<{ id: UserId | undefined } & AccountInfo>;
|
activeAccount$: Observable<{ id: UserId | undefined } & AccountInfo>;
|
||||||
accountLock$: Observable<UserId>;
|
|
||||||
accountLogout$: Observable<UserId>;
|
|
||||||
/**
|
/**
|
||||||
* Updates the `accounts$` observable with the new account data.
|
* Updates the `accounts$` observable with the new account data.
|
||||||
* @param userId
|
* @param userId
|
||||||
@ -40,24 +36,6 @@ export abstract class AccountService {
|
|||||||
* @param email
|
* @param email
|
||||||
*/
|
*/
|
||||||
abstract setAccountEmail(userId: UserId, email: string): Promise<void>;
|
abstract setAccountEmail(userId: UserId, email: string): Promise<void>;
|
||||||
/**
|
|
||||||
* Updates the `accounts$` observable with the new account status.
|
|
||||||
* Also emits the `accountLock$` or `accountLogout$` observable if the status is `Locked` or `LoggedOut` respectively.
|
|
||||||
* @param userId
|
|
||||||
* @param status
|
|
||||||
*/
|
|
||||||
abstract setAccountStatus(userId: UserId, status: AuthenticationStatus): Promise<void>;
|
|
||||||
/**
|
|
||||||
* Updates the `accounts$` observable with the new account status if the current status is higher than the `maxStatus`.
|
|
||||||
*
|
|
||||||
* This method only downgrades status to the maximum value sent in, it will not increase authentication status.
|
|
||||||
*
|
|
||||||
* @example An account is transitioning from unlocked to logged out. If callbacks that set the status to locked occur
|
|
||||||
* after it is updated to logged out, the account will be in the incorrect state.
|
|
||||||
* @param userId The user id of the account to be updated.
|
|
||||||
* @param maxStatus The new status of the account.
|
|
||||||
*/
|
|
||||||
abstract setMaxAccountStatus(userId: UserId, maxStatus: AuthenticationStatus): Promise<void>;
|
|
||||||
/**
|
/**
|
||||||
* Updates the `activeAccount$` observable with the new active account.
|
* Updates the `activeAccount$` observable with the new active account.
|
||||||
* @param userId
|
* @param userId
|
||||||
|
@ -6,6 +6,8 @@ import { AuthenticationStatus } from "../enums/authentication-status";
|
|||||||
export abstract class AuthService {
|
export abstract class AuthService {
|
||||||
/** Authentication status for the active user */
|
/** Authentication status for the active user */
|
||||||
abstract activeAccountStatus$: Observable<AuthenticationStatus>;
|
abstract activeAccountStatus$: Observable<AuthenticationStatus>;
|
||||||
|
/** Authentication status for all known users */
|
||||||
|
abstract authStatuses$: Observable<Record<UserId, AuthenticationStatus>>;
|
||||||
/**
|
/**
|
||||||
* Returns an observable authentication status for the given user id.
|
* Returns an observable authentication status for the given user id.
|
||||||
* @note userId is a required parameter, null values will always return `AuthenticationStatus.LoggedOut`
|
* @note userId is a required parameter, null values will always return `AuthenticationStatus.LoggedOut`
|
||||||
|
@ -8,7 +8,6 @@ import { LogService } from "../../platform/abstractions/log.service";
|
|||||||
import { MessagingService } from "../../platform/abstractions/messaging.service";
|
import { MessagingService } from "../../platform/abstractions/messaging.service";
|
||||||
import { UserId } from "../../types/guid";
|
import { UserId } from "../../types/guid";
|
||||||
import { AccountInfo } from "../abstractions/account.service";
|
import { AccountInfo } from "../abstractions/account.service";
|
||||||
import { AuthenticationStatus } from "../enums/authentication-status";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ACCOUNT_ACCOUNTS,
|
ACCOUNT_ACCOUNTS,
|
||||||
@ -24,9 +23,7 @@ describe("accountService", () => {
|
|||||||
let accountsState: FakeGlobalState<Record<UserId, AccountInfo>>;
|
let accountsState: FakeGlobalState<Record<UserId, AccountInfo>>;
|
||||||
let activeAccountIdState: FakeGlobalState<UserId>;
|
let activeAccountIdState: FakeGlobalState<UserId>;
|
||||||
const userId = "userId" as UserId;
|
const userId = "userId" as UserId;
|
||||||
function userInfo(status: AuthenticationStatus): AccountInfo {
|
const userInfo = { email: "email", name: "name" };
|
||||||
return { status, email: "email", name: "name" };
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
messagingService = mock();
|
messagingService = mock();
|
||||||
@ -50,61 +47,49 @@ describe("accountService", () => {
|
|||||||
expect(emissions).toEqual([undefined]);
|
expect(emissions).toEqual([undefined]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should emit the active account and status", async () => {
|
it("should emit the active account", async () => {
|
||||||
const emissions = trackEmissions(sut.activeAccount$);
|
const emissions = trackEmissions(sut.activeAccount$);
|
||||||
accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Unlocked) });
|
accountsState.stateSubject.next({ [userId]: userInfo });
|
||||||
activeAccountIdState.stateSubject.next(userId);
|
activeAccountIdState.stateSubject.next(userId);
|
||||||
|
|
||||||
expect(emissions).toEqual([
|
expect(emissions).toEqual([
|
||||||
undefined, // initial value
|
undefined, // initial value
|
||||||
{ id: userId, ...userInfo(AuthenticationStatus.Unlocked) },
|
{ id: userId, ...userInfo },
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should update the status if the account status changes", async () => {
|
|
||||||
accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Unlocked) });
|
|
||||||
activeAccountIdState.stateSubject.next(userId);
|
|
||||||
const emissions = trackEmissions(sut.activeAccount$);
|
|
||||||
accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Locked) });
|
|
||||||
|
|
||||||
expect(emissions).toEqual([
|
|
||||||
{ id: userId, ...userInfo(AuthenticationStatus.Unlocked) },
|
|
||||||
{ id: userId, ...userInfo(AuthenticationStatus.Locked) },
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should remember the last emitted value", async () => {
|
it("should remember the last emitted value", async () => {
|
||||||
accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Unlocked) });
|
accountsState.stateSubject.next({ [userId]: userInfo });
|
||||||
activeAccountIdState.stateSubject.next(userId);
|
activeAccountIdState.stateSubject.next(userId);
|
||||||
|
|
||||||
expect(await firstValueFrom(sut.activeAccount$)).toEqual({
|
expect(await firstValueFrom(sut.activeAccount$)).toEqual({
|
||||||
id: userId,
|
id: userId,
|
||||||
...userInfo(AuthenticationStatus.Unlocked),
|
...userInfo,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("accounts$", () => {
|
describe("accounts$", () => {
|
||||||
it("should maintain an accounts cache", async () => {
|
it("should maintain an accounts cache", async () => {
|
||||||
accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Unlocked) });
|
accountsState.stateSubject.next({ [userId]: userInfo });
|
||||||
accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Locked) });
|
accountsState.stateSubject.next({ [userId]: userInfo });
|
||||||
expect(await firstValueFrom(sut.accounts$)).toEqual({
|
expect(await firstValueFrom(sut.accounts$)).toEqual({
|
||||||
[userId]: userInfo(AuthenticationStatus.Locked),
|
[userId]: userInfo,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("addAccount", () => {
|
describe("addAccount", () => {
|
||||||
it("should emit the new account", async () => {
|
it("should emit the new account", async () => {
|
||||||
await sut.addAccount(userId, userInfo(AuthenticationStatus.Unlocked));
|
await sut.addAccount(userId, userInfo);
|
||||||
const currentValue = await firstValueFrom(sut.accounts$);
|
const currentValue = await firstValueFrom(sut.accounts$);
|
||||||
|
|
||||||
expect(currentValue).toEqual({ [userId]: userInfo(AuthenticationStatus.Unlocked) });
|
expect(currentValue).toEqual({ [userId]: userInfo });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("setAccountName", () => {
|
describe("setAccountName", () => {
|
||||||
const initialState = { [userId]: userInfo(AuthenticationStatus.Unlocked) };
|
const initialState = { [userId]: userInfo };
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
accountsState.stateSubject.next(initialState);
|
accountsState.stateSubject.next(initialState);
|
||||||
});
|
});
|
||||||
@ -114,7 +99,7 @@ describe("accountService", () => {
|
|||||||
const currentState = await firstValueFrom(accountsState.state$);
|
const currentState = await firstValueFrom(accountsState.state$);
|
||||||
|
|
||||||
expect(currentState).toEqual({
|
expect(currentState).toEqual({
|
||||||
[userId]: { ...userInfo(AuthenticationStatus.Unlocked), name: "new name" },
|
[userId]: { ...userInfo, name: "new name" },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -127,7 +112,7 @@ describe("accountService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("setAccountEmail", () => {
|
describe("setAccountEmail", () => {
|
||||||
const initialState = { [userId]: userInfo(AuthenticationStatus.Unlocked) };
|
const initialState = { [userId]: userInfo };
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
accountsState.stateSubject.next(initialState);
|
accountsState.stateSubject.next(initialState);
|
||||||
});
|
});
|
||||||
@ -137,7 +122,7 @@ describe("accountService", () => {
|
|||||||
const currentState = await firstValueFrom(accountsState.state$);
|
const currentState = await firstValueFrom(accountsState.state$);
|
||||||
|
|
||||||
expect(currentState).toEqual({
|
expect(currentState).toEqual({
|
||||||
[userId]: { ...userInfo(AuthenticationStatus.Unlocked), email: "new email" },
|
[userId]: { ...userInfo, email: "new email" },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -149,49 +134,9 @@ describe("accountService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("setAccountStatus", () => {
|
|
||||||
const initialState = { [userId]: userInfo(AuthenticationStatus.Unlocked) };
|
|
||||||
beforeEach(() => {
|
|
||||||
accountsState.stateSubject.next(initialState);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should update the account", async () => {
|
|
||||||
await sut.setAccountStatus(userId, AuthenticationStatus.Locked);
|
|
||||||
const currentState = await firstValueFrom(accountsState.state$);
|
|
||||||
|
|
||||||
expect(currentState).toEqual({
|
|
||||||
[userId]: {
|
|
||||||
...userInfo(AuthenticationStatus.Unlocked),
|
|
||||||
status: AuthenticationStatus.Locked,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not update if the status is the same", async () => {
|
|
||||||
await sut.setAccountStatus(userId, AuthenticationStatus.Unlocked);
|
|
||||||
const currentState = await firstValueFrom(accountsState.state$);
|
|
||||||
|
|
||||||
expect(currentState).toEqual(initialState);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should emit logout if the status is logged out", async () => {
|
|
||||||
const emissions = trackEmissions(sut.accountLogout$);
|
|
||||||
await sut.setAccountStatus(userId, AuthenticationStatus.LoggedOut);
|
|
||||||
|
|
||||||
expect(emissions).toEqual([userId]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should emit lock if the status is locked", async () => {
|
|
||||||
const emissions = trackEmissions(sut.accountLock$);
|
|
||||||
await sut.setAccountStatus(userId, AuthenticationStatus.Locked);
|
|
||||||
|
|
||||||
expect(emissions).toEqual([userId]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("switchAccount", () => {
|
describe("switchAccount", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Unlocked) });
|
accountsState.stateSubject.next({ [userId]: userInfo });
|
||||||
activeAccountIdState.stateSubject.next(userId);
|
activeAccountIdState.stateSubject.next(userId);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -207,26 +152,4 @@ describe("accountService", () => {
|
|||||||
expect(sut.switchAccount("unknown" as UserId)).rejects.toThrowError("Account does not exist");
|
expect(sut.switchAccount("unknown" as UserId)).rejects.toThrowError("Account does not exist");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("setMaxAccountStatus", () => {
|
|
||||||
it("should update the account", async () => {
|
|
||||||
accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Unlocked) });
|
|
||||||
await sut.setMaxAccountStatus(userId, AuthenticationStatus.Locked);
|
|
||||||
const currentState = await firstValueFrom(accountsState.state$);
|
|
||||||
|
|
||||||
expect(currentState).toEqual({
|
|
||||||
[userId]: userInfo(AuthenticationStatus.Locked),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not update if the new max status is higher than the current", async () => {
|
|
||||||
accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.LoggedOut) });
|
|
||||||
await sut.setMaxAccountStatus(userId, AuthenticationStatus.Locked);
|
|
||||||
const currentState = await firstValueFrom(accountsState.state$);
|
|
||||||
|
|
||||||
expect(currentState).toEqual({
|
|
||||||
[userId]: userInfo(AuthenticationStatus.LoggedOut),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -14,7 +14,6 @@ import {
|
|||||||
KeyDefinition,
|
KeyDefinition,
|
||||||
} from "../../platform/state";
|
} from "../../platform/state";
|
||||||
import { UserId } from "../../types/guid";
|
import { UserId } from "../../types/guid";
|
||||||
import { AuthenticationStatus } from "../enums/authentication-status";
|
|
||||||
|
|
||||||
export const ACCOUNT_ACCOUNTS = KeyDefinition.record<AccountInfo, UserId>(
|
export const ACCOUNT_ACCOUNTS = KeyDefinition.record<AccountInfo, UserId>(
|
||||||
ACCOUNT_MEMORY,
|
ACCOUNT_MEMORY,
|
||||||
@ -36,8 +35,6 @@ export class AccountServiceImplementation implements InternalAccountService {
|
|||||||
|
|
||||||
accounts$;
|
accounts$;
|
||||||
activeAccount$;
|
activeAccount$;
|
||||||
accountLock$ = this.lock.asObservable();
|
|
||||||
accountLogout$ = this.logout.asObservable();
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
@ -74,34 +71,6 @@ export class AccountServiceImplementation implements InternalAccountService {
|
|||||||
await this.setAccountInfo(userId, { email });
|
await this.setAccountInfo(userId, { email });
|
||||||
}
|
}
|
||||||
|
|
||||||
async setAccountStatus(userId: UserId, status: AuthenticationStatus): Promise<void> {
|
|
||||||
await this.setAccountInfo(userId, { status });
|
|
||||||
|
|
||||||
if (status === AuthenticationStatus.LoggedOut) {
|
|
||||||
this.logout.next(userId);
|
|
||||||
} else if (status === AuthenticationStatus.Locked) {
|
|
||||||
this.lock.next(userId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async setMaxAccountStatus(userId: UserId, maxStatus: AuthenticationStatus): Promise<void> {
|
|
||||||
await this.accountsState.update(
|
|
||||||
(accounts) => {
|
|
||||||
accounts[userId].status = maxStatus;
|
|
||||||
return accounts;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
shouldUpdate: (accounts) => {
|
|
||||||
if (accounts?.[userId] == null) {
|
|
||||||
throw new Error("Account does not exist");
|
|
||||||
}
|
|
||||||
|
|
||||||
return accounts[userId].status > maxStatus;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async switchAccount(userId: UserId): Promise<void> {
|
async switchAccount(userId: UserId): Promise<void> {
|
||||||
await this.activeAccountIdState.update(
|
await this.activeAccountIdState.update(
|
||||||
(_, accounts) => {
|
(_, accounts) => {
|
||||||
|
@ -122,6 +122,25 @@ describe("AuthService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("authStatuses$", () => {
|
||||||
|
it("requests auth status for all known users", async () => {
|
||||||
|
const userId2 = Utils.newGuid() as UserId;
|
||||||
|
|
||||||
|
await accountService.addAccount(userId2, { email: "email2", name: "name2" });
|
||||||
|
|
||||||
|
const mockFn = jest.fn().mockReturnValue(of(AuthenticationStatus.Locked));
|
||||||
|
sut.authStatusFor$ = mockFn;
|
||||||
|
|
||||||
|
await expect(firstValueFrom(await sut.authStatuses$)).resolves.toEqual({
|
||||||
|
[userId]: AuthenticationStatus.Locked,
|
||||||
|
[userId2]: AuthenticationStatus.Locked,
|
||||||
|
});
|
||||||
|
expect(mockFn).toHaveBeenCalledTimes(2);
|
||||||
|
expect(mockFn).toHaveBeenCalledWith(userId);
|
||||||
|
expect(mockFn).toHaveBeenCalledWith(userId2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("authStatusFor$", () => {
|
describe("authStatusFor$", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
tokenService.hasAccessToken$.mockReturnValue(of(true));
|
tokenService.hasAccessToken$.mockReturnValue(of(true));
|
||||||
|
@ -21,6 +21,7 @@ import { AuthenticationStatus } from "../enums/authentication-status";
|
|||||||
|
|
||||||
export class AuthService implements AuthServiceAbstraction {
|
export class AuthService implements AuthServiceAbstraction {
|
||||||
activeAccountStatus$: Observable<AuthenticationStatus>;
|
activeAccountStatus$: Observable<AuthenticationStatus>;
|
||||||
|
authStatuses$: Observable<Record<UserId, AuthenticationStatus>>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected accountService: AccountService,
|
protected accountService: AccountService,
|
||||||
@ -36,6 +37,26 @@ export class AuthService implements AuthServiceAbstraction {
|
|||||||
return this.authStatusFor$(userId);
|
return this.authStatusFor$(userId);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.authStatuses$ = this.accountService.accounts$.pipe(
|
||||||
|
map((accounts) => Object.keys(accounts) as UserId[]),
|
||||||
|
switchMap((entries) =>
|
||||||
|
combineLatest(
|
||||||
|
entries.map((userId) =>
|
||||||
|
this.authStatusFor$(userId).pipe(map((status) => ({ userId, status }))),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
map((statuses) => {
|
||||||
|
return statuses.reduce(
|
||||||
|
(acc, { userId, status }) => {
|
||||||
|
acc[userId] = status;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<UserId, AuthenticationStatus>,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
authStatusFor$(userId: UserId): Observable<AuthenticationStatus> {
|
authStatusFor$(userId: UserId): Observable<AuthenticationStatus> {
|
||||||
|
@ -8,7 +8,6 @@ import { OrganizationAutoEnrollStatusResponse } from "../../admin-console/models
|
|||||||
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
||||||
import { I18nService } from "../../platform/abstractions/i18n.service";
|
import { I18nService } from "../../platform/abstractions/i18n.service";
|
||||||
import { AccountInfo, AccountService } from "../abstractions/account.service";
|
import { AccountInfo, AccountService } from "../abstractions/account.service";
|
||||||
import { AuthenticationStatus } from "../enums/authentication-status";
|
|
||||||
|
|
||||||
import { PasswordResetEnrollmentServiceImplementation } from "./password-reset-enrollment.service.implementation";
|
import { PasswordResetEnrollmentServiceImplementation } from "./password-reset-enrollment.service.implementation";
|
||||||
|
|
||||||
@ -91,7 +90,6 @@ describe("PasswordResetEnrollmentServiceImplementation", () => {
|
|||||||
const user1AccountInfo: AccountInfo = {
|
const user1AccountInfo: AccountInfo = {
|
||||||
name: "Test User 1",
|
name: "Test User 1",
|
||||||
email: "test1@email.com",
|
email: "test1@email.com",
|
||||||
status: AuthenticationStatus.Unlocked,
|
|
||||||
};
|
};
|
||||||
activeAccountSubject.next(Object.assign(user1AccountInfo, { id: "userId" as UserId }));
|
activeAccountSubject.next(Object.assign(user1AccountInfo, { id: "userId" as UserId }));
|
||||||
|
|
||||||
|
@ -33,10 +33,6 @@ export type InitOptions = {
|
|||||||
export abstract class StateService<T extends Account = Account> {
|
export abstract class StateService<T extends Account = Account> {
|
||||||
accounts$: Observable<{ [userId: string]: T }>;
|
accounts$: Observable<{ [userId: string]: T }>;
|
||||||
activeAccount$: Observable<string>;
|
activeAccount$: Observable<string>;
|
||||||
/**
|
|
||||||
* @deprecated use accountService.activeAccount$ instead
|
|
||||||
*/
|
|
||||||
activeAccountUnlocked$: Observable<boolean>;
|
|
||||||
|
|
||||||
addAccount: (account: T) => Promise<void>;
|
addAccount: (account: T) => Promise<void>;
|
||||||
setActiveUser: (userId: string) => Promise<void>;
|
setActiveUser: (userId: string) => Promise<void>;
|
||||||
|
@ -4,7 +4,6 @@ import { firstValueFrom, of, tap } from "rxjs";
|
|||||||
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
|
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
|
||||||
import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state";
|
import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state";
|
||||||
import { FakeStateProvider } from "../../../spec/fake-state-provider";
|
import { FakeStateProvider } from "../../../spec/fake-state-provider";
|
||||||
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
|
|
||||||
import { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service";
|
import { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service";
|
||||||
import { CsprngArray } from "../../types/csprng";
|
import { CsprngArray } from "../../types/csprng";
|
||||||
import { UserId } from "../../types/guid";
|
import { UserId } from "../../types/guid";
|
||||||
@ -273,15 +272,6 @@ describe("cryptoService", () => {
|
|||||||
await expect(cryptoService.setUserKey(null, mockUserId)).rejects.toThrow("No key provided.");
|
await expect(cryptoService.setUserKey(null, mockUserId)).rejects.toThrow("No key provided.");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should update the user's lock state", async () => {
|
|
||||||
await cryptoService.setUserKey(mockUserKey, mockUserId);
|
|
||||||
|
|
||||||
expect(accountService.mock.setAccountStatus).toHaveBeenCalledWith(
|
|
||||||
mockUserId,
|
|
||||||
AuthenticationStatus.Unlocked,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Pin Key refresh", () => {
|
describe("Pin Key refresh", () => {
|
||||||
let cryptoSvcMakePinKey: jest.SpyInstance;
|
let cryptoSvcMakePinKey: jest.SpyInstance;
|
||||||
const protectedPin =
|
const protectedPin =
|
||||||
@ -353,23 +343,6 @@ describe("cryptoService", () => {
|
|||||||
accountService.activeAccount$ = accountService.activeAccountSubject.asObservable();
|
accountService.activeAccount$ = accountService.activeAccountSubject.asObservable();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sets the maximum account status of the active user id to locked when user id is not specified", async () => {
|
|
||||||
await cryptoService.clearKeys();
|
|
||||||
expect(accountService.mock.setMaxAccountStatus).toHaveBeenCalledWith(
|
|
||||||
mockUserId,
|
|
||||||
AuthenticationStatus.Locked,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sets the maximum account status of the specified user id to locked when user id is specified", async () => {
|
|
||||||
const userId = "someOtherUser" as UserId;
|
|
||||||
await cryptoService.clearKeys(userId);
|
|
||||||
expect(accountService.mock.setMaxAccountStatus).toHaveBeenCalledWith(
|
|
||||||
userId,
|
|
||||||
AuthenticationStatus.Locked,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe.each([
|
describe.each([
|
||||||
USER_ENCRYPTED_ORGANIZATION_KEYS,
|
USER_ENCRYPTED_ORGANIZATION_KEYS,
|
||||||
USER_ENCRYPTED_PROVIDER_KEYS,
|
USER_ENCRYPTED_PROVIDER_KEYS,
|
||||||
|
@ -7,7 +7,6 @@ import { ProfileProviderOrganizationResponse } from "../../admin-console/models/
|
|||||||
import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response";
|
import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response";
|
||||||
import { AccountService } from "../../auth/abstractions/account.service";
|
import { AccountService } from "../../auth/abstractions/account.service";
|
||||||
import { InternalMasterPasswordServiceAbstraction } from "../../auth/abstractions/master-password.service.abstraction";
|
import { InternalMasterPasswordServiceAbstraction } from "../../auth/abstractions/master-password.service.abstraction";
|
||||||
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
|
|
||||||
import { KdfConfig } from "../../auth/models/domain/kdf-config";
|
import { KdfConfig } from "../../auth/models/domain/kdf-config";
|
||||||
import { Utils } from "../../platform/misc/utils";
|
import { Utils } from "../../platform/misc/utils";
|
||||||
import { CsprngArray } from "../../types/csprng";
|
import { CsprngArray } from "../../types/csprng";
|
||||||
@ -152,8 +151,6 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
[userId, key] = await this.stateProvider.setUserState(USER_KEY, key, userId);
|
[userId, key] = await this.stateProvider.setUserState(USER_KEY, key, userId);
|
||||||
await this.stateProvider.setUserState(USER_EVER_HAD_USER_KEY, true, userId);
|
await this.stateProvider.setUserState(USER_EVER_HAD_USER_KEY, true, userId);
|
||||||
|
|
||||||
await this.accountService.setAccountStatus(userId, AuthenticationStatus.Unlocked);
|
|
||||||
|
|
||||||
await this.storeAdditionalKeys(key, userId);
|
await this.storeAdditionalKeys(key, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -256,14 +253,13 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
* Clears the user key. Clears all stored versions of the user keys as well, such as the biometrics key
|
* Clears the user key. Clears all stored versions of the user keys as well, such as the biometrics key
|
||||||
* @param userId The desired user
|
* @param userId The desired user
|
||||||
*/
|
*/
|
||||||
async clearUserKey(userId: UserId): Promise<void> {
|
private async clearUserKey(userId: UserId): Promise<void> {
|
||||||
if (userId == null) {
|
if (userId == null) {
|
||||||
// nothing to do
|
// nothing to do
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Set userId to ensure we have one for the account status update
|
// Set userId to ensure we have one for the account status update
|
||||||
await this.stateProvider.setUserState(USER_KEY, null, userId);
|
await this.stateProvider.setUserState(USER_KEY, null, userId);
|
||||||
await this.accountService.setMaxAccountStatus(userId, AuthenticationStatus.Locked);
|
|
||||||
await this.clearAllStoredUserKeys(userId);
|
await this.clearAllStoredUserKeys(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@ import { firstValueFrom } from "rxjs";
|
|||||||
|
|
||||||
import { FakeStateProvider, awaitAsync } from "../../../spec";
|
import { FakeStateProvider, awaitAsync } from "../../../spec";
|
||||||
import { FakeAccountService } from "../../../spec/fake-account-service";
|
import { FakeAccountService } from "../../../spec/fake-account-service";
|
||||||
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
|
|
||||||
import { UserId } from "../../types/guid";
|
import { UserId } from "../../types/guid";
|
||||||
import { CloudRegion, Region } from "../abstractions/environment.service";
|
import { CloudRegion, Region } from "../abstractions/environment.service";
|
||||||
|
|
||||||
@ -32,12 +31,10 @@ describe("EnvironmentService", () => {
|
|||||||
[testUser]: {
|
[testUser]: {
|
||||||
name: "name",
|
name: "name",
|
||||||
email: "email",
|
email: "email",
|
||||||
status: AuthenticationStatus.Locked,
|
|
||||||
},
|
},
|
||||||
[alternateTestUser]: {
|
[alternateTestUser]: {
|
||||||
name: "name",
|
name: "name",
|
||||||
email: "email",
|
email: "email",
|
||||||
status: AuthenticationStatus.Locked,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
stateProvider = new FakeStateProvider(accountService);
|
stateProvider = new FakeStateProvider(accountService);
|
||||||
@ -50,7 +47,6 @@ describe("EnvironmentService", () => {
|
|||||||
id: userId,
|
id: userId,
|
||||||
email: "test@example.com",
|
email: "test@example.com",
|
||||||
name: `Test Name ${userId}`,
|
name: `Test Name ${userId}`,
|
||||||
status: AuthenticationStatus.Unlocked,
|
|
||||||
});
|
});
|
||||||
await awaitAsync();
|
await awaitAsync();
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { BehaviorSubject, Observable, map } from "rxjs";
|
import { BehaviorSubject } from "rxjs";
|
||||||
import { Jsonify, JsonValue } from "type-fest";
|
import { Jsonify, JsonValue } from "type-fest";
|
||||||
|
|
||||||
import { AccountService } from "../../auth/abstractions/account.service";
|
import { AccountService } from "../../auth/abstractions/account.service";
|
||||||
import { TokenService } from "../../auth/abstractions/token.service";
|
import { TokenService } from "../../auth/abstractions/token.service";
|
||||||
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
|
|
||||||
import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable";
|
import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable";
|
||||||
import { KdfConfig } from "../../auth/models/domain/kdf-config";
|
import { KdfConfig } from "../../auth/models/domain/kdf-config";
|
||||||
import { BiometricKey } from "../../auth/types/biometric-key";
|
import { BiometricKey } from "../../auth/types/biometric-key";
|
||||||
@ -68,8 +67,6 @@ export class StateService<
|
|||||||
protected activeAccountSubject = new BehaviorSubject<string | null>(null);
|
protected activeAccountSubject = new BehaviorSubject<string | null>(null);
|
||||||
activeAccount$ = this.activeAccountSubject.asObservable();
|
activeAccount$ = this.activeAccountSubject.asObservable();
|
||||||
|
|
||||||
activeAccountUnlocked$: Observable<boolean>;
|
|
||||||
|
|
||||||
private hasBeenInited = false;
|
private hasBeenInited = false;
|
||||||
protected isRecoveredSession = false;
|
protected isRecoveredSession = false;
|
||||||
|
|
||||||
@ -89,13 +86,7 @@ export class StateService<
|
|||||||
protected tokenService: TokenService,
|
protected tokenService: TokenService,
|
||||||
private migrationRunner: MigrationRunner,
|
private migrationRunner: MigrationRunner,
|
||||||
protected useAccountCache: boolean = true,
|
protected useAccountCache: boolean = true,
|
||||||
) {
|
) {}
|
||||||
this.activeAccountUnlocked$ = this.accountService.activeAccount$.pipe(
|
|
||||||
map((a) => {
|
|
||||||
return a?.status === AuthenticationStatus.Unlocked;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async init(initOptions: InitOptions = {}): Promise<void> {
|
async init(initOptions: InitOptions = {}): Promise<void> {
|
||||||
// Deconstruct and apply defaults
|
// Deconstruct and apply defaults
|
||||||
@ -151,7 +142,6 @@ export class StateService<
|
|||||||
await this.accountService.addAccount(state.activeUserId as UserId, {
|
await this.accountService.addAccount(state.activeUserId as UserId, {
|
||||||
name: activeDiskAccount.profile.name,
|
name: activeDiskAccount.profile.name,
|
||||||
email: activeDiskAccount.profile.email,
|
email: activeDiskAccount.profile.email,
|
||||||
status: AuthenticationStatus.LoggedOut,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await this.accountService.switchAccount(state.activeUserId as UserId);
|
await this.accountService.switchAccount(state.activeUserId as UserId);
|
||||||
@ -177,16 +167,7 @@ export class StateService<
|
|||||||
|
|
||||||
// TODO: Temporary update to avoid routing all account status changes through account service for now.
|
// TODO: Temporary update to avoid routing all account status changes through account service for now.
|
||||||
// The determination of state should be handled by the various services that control those values.
|
// The determination of state should be handled by the various services that control those values.
|
||||||
const token = await this.tokenService.getAccessToken(userId as UserId);
|
|
||||||
const autoKey = await this.getUserKeyAutoUnlock({ userId: userId });
|
|
||||||
const accountStatus =
|
|
||||||
token == null
|
|
||||||
? AuthenticationStatus.LoggedOut
|
|
||||||
: autoKey == null
|
|
||||||
? AuthenticationStatus.Locked
|
|
||||||
: AuthenticationStatus.Unlocked;
|
|
||||||
await this.accountService.addAccount(userId as UserId, {
|
await this.accountService.addAccount(userId as UserId, {
|
||||||
status: accountStatus,
|
|
||||||
name: diskAccount.profile.name,
|
name: diskAccount.profile.name,
|
||||||
email: diskAccount.profile.email,
|
email: diskAccount.profile.email,
|
||||||
});
|
});
|
||||||
@ -206,7 +187,6 @@ export class StateService<
|
|||||||
await this.setLastActive(new Date().getTime(), { userId: account.profile.userId });
|
await this.setLastActive(new Date().getTime(), { userId: account.profile.userId });
|
||||||
// TODO: Temporary update to avoid routing all account status changes through account service for now.
|
// TODO: Temporary update to avoid routing all account status changes through account service for now.
|
||||||
await this.accountService.addAccount(account.profile.userId as UserId, {
|
await this.accountService.addAccount(account.profile.userId as UserId, {
|
||||||
status: AuthenticationStatus.Locked,
|
|
||||||
name: account.profile.name,
|
name: account.profile.name,
|
||||||
email: account.profile.email,
|
email: account.profile.email,
|
||||||
});
|
});
|
||||||
@ -1406,8 +1386,6 @@ export class StateService<
|
|||||||
|
|
||||||
return state;
|
return state;
|
||||||
});
|
});
|
||||||
// TODO: Invert this logic, we should remove accounts based on logged out emit
|
|
||||||
await this.accountService.setAccountStatus(userId as UserId, AuthenticationStatus.LoggedOut);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// settings persist even on reset, and are not affected by this method
|
// settings persist even on reset, and are not affected by this method
|
||||||
|
@ -9,7 +9,6 @@ import { Jsonify } from "type-fest";
|
|||||||
import { awaitAsync, trackEmissions } from "../../../../spec";
|
import { awaitAsync, trackEmissions } from "../../../../spec";
|
||||||
import { FakeStorageService } from "../../../../spec/fake-storage.service";
|
import { FakeStorageService } from "../../../../spec/fake-storage.service";
|
||||||
import { AccountInfo } from "../../../auth/abstractions/account.service";
|
import { AccountInfo } from "../../../auth/abstractions/account.service";
|
||||||
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
|
||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
import { StorageServiceProvider } from "../../services/storage-service.provider";
|
import { StorageServiceProvider } from "../../services/storage-service.provider";
|
||||||
import { StateDefinition } from "../state-definition";
|
import { StateDefinition } from "../state-definition";
|
||||||
@ -84,7 +83,6 @@ describe("DefaultActiveUserState", () => {
|
|||||||
id: userId,
|
id: userId,
|
||||||
email: `test${id}@example.com`,
|
email: `test${id}@example.com`,
|
||||||
name: `Test User ${id}`,
|
name: `Test User ${id}`,
|
||||||
status: AuthenticationStatus.Unlocked,
|
|
||||||
});
|
});
|
||||||
await awaitAsync();
|
await awaitAsync();
|
||||||
};
|
};
|
||||||
|
@ -3,7 +3,7 @@ import { firstValueFrom, map, from, zip } from "rxjs";
|
|||||||
import { EventCollectionService as EventCollectionServiceAbstraction } from "../../abstractions/event/event-collection.service";
|
import { EventCollectionService as EventCollectionServiceAbstraction } from "../../abstractions/event/event-collection.service";
|
||||||
import { EventUploadService } from "../../abstractions/event/event-upload.service";
|
import { EventUploadService } from "../../abstractions/event/event-upload.service";
|
||||||
import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { AccountService } from "../../auth/abstractions/account.service";
|
import { AuthService } from "../../auth/abstractions/auth.service";
|
||||||
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
|
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
|
||||||
import { EventType } from "../../enums";
|
import { EventType } from "../../enums";
|
||||||
import { EventData } from "../../models/data/event.data";
|
import { EventData } from "../../models/data/event.data";
|
||||||
@ -18,7 +18,7 @@ export class EventCollectionService implements EventCollectionServiceAbstraction
|
|||||||
private stateProvider: StateProvider,
|
private stateProvider: StateProvider,
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
private eventUploadService: EventUploadService,
|
private eventUploadService: EventUploadService,
|
||||||
private accountService: AccountService,
|
private authService: AuthService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/** Adds an event to the active user's event collection
|
/** Adds an event to the active user's event collection
|
||||||
@ -71,12 +71,12 @@ export class EventCollectionService implements EventCollectionServiceAbstraction
|
|||||||
|
|
||||||
const cipher$ = from(this.cipherService.get(cipherId));
|
const cipher$ = from(this.cipherService.get(cipherId));
|
||||||
|
|
||||||
const [accountInfo, orgIds, cipher] = await firstValueFrom(
|
const [authStatus, orgIds, cipher] = await firstValueFrom(
|
||||||
zip(this.accountService.activeAccount$, orgIds$, cipher$),
|
zip(this.authService.activeAccountStatus$, orgIds$, cipher$),
|
||||||
);
|
);
|
||||||
|
|
||||||
// The user must be authorized
|
// The user must be authorized
|
||||||
if (accountInfo.status != AuthenticationStatus.Unlocked) {
|
if (authStatus != AuthenticationStatus.Unlocked) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { firstValueFrom, map } from "rxjs";
|
|||||||
|
|
||||||
import { ApiService } from "../../abstractions/api.service";
|
import { ApiService } from "../../abstractions/api.service";
|
||||||
import { EventUploadService as EventUploadServiceAbstraction } from "../../abstractions/event/event-upload.service";
|
import { EventUploadService as EventUploadServiceAbstraction } from "../../abstractions/event/event-upload.service";
|
||||||
import { AccountService } from "../../auth/abstractions/account.service";
|
import { AuthService } from "../../auth/abstractions/auth.service";
|
||||||
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
|
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
|
||||||
import { EventData } from "../../models/data/event.data";
|
import { EventData } from "../../models/data/event.data";
|
||||||
import { EventRequest } from "../../models/request/event.request";
|
import { EventRequest } from "../../models/request/event.request";
|
||||||
@ -18,7 +18,7 @@ export class EventUploadService implements EventUploadServiceAbstraction {
|
|||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private stateProvider: StateProvider,
|
private stateProvider: StateProvider,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private accountService: AccountService,
|
private authService: AuthService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
init(checkOnInterval: boolean) {
|
init(checkOnInterval: boolean) {
|
||||||
@ -43,13 +43,16 @@ export class EventUploadService implements EventUploadServiceAbstraction {
|
|||||||
userId = await firstValueFrom(this.stateProvider.activeUserId$);
|
userId = await firstValueFrom(this.stateProvider.activeUserId$);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the auth status from the provided user or the active user
|
if (!userId) {
|
||||||
const userAuth$ = this.accountService.accounts$.pipe(
|
return;
|
||||||
map((accounts) => accounts[userId]?.status === AuthenticationStatus.Unlocked),
|
}
|
||||||
);
|
|
||||||
|
|
||||||
const isAuthenticated = await firstValueFrom(userAuth$);
|
const isUnlocked = await firstValueFrom(
|
||||||
if (!isAuthenticated) {
|
this.authService
|
||||||
|
.authStatusFor$(userId)
|
||||||
|
.pipe(map((status) => status === AuthenticationStatus.Unlocked)),
|
||||||
|
);
|
||||||
|
if (!isUnlocked) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,7 +138,6 @@ describe("VaultTimeoutService", () => {
|
|||||||
if (globalSetups?.userId) {
|
if (globalSetups?.userId) {
|
||||||
accountService.activeAccountSubject.next({
|
accountService.activeAccountSubject.next({
|
||||||
id: globalSetups.userId as UserId,
|
id: globalSetups.userId as UserId,
|
||||||
status: accounts[globalSetups.userId]?.authStatus,
|
|
||||||
email: null,
|
email: null,
|
||||||
name: null,
|
name: null,
|
||||||
});
|
});
|
||||||
|
@ -8,7 +8,6 @@ import {
|
|||||||
awaitAsync,
|
awaitAsync,
|
||||||
mockAccountServiceWith,
|
mockAccountServiceWith,
|
||||||
} from "../../../../spec";
|
} from "../../../../spec";
|
||||||
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
|
||||||
import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
||||||
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
|
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
|
||||||
import { I18nService } from "../../../platform/abstractions/i18n.service";
|
import { I18nService } from "../../../platform/abstractions/i18n.service";
|
||||||
@ -64,7 +63,6 @@ describe("SendService", () => {
|
|||||||
id: mockUserId,
|
id: mockUserId,
|
||||||
email: "email",
|
email: "email",
|
||||||
name: "name",
|
name: "name",
|
||||||
status: AuthenticationStatus.Unlocked,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initial encrypted state
|
// Initial encrypted state
|
||||||
|
Loading…
Reference in New Issue
Block a user