1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-14 15:23:33 +00:00

[PM-6688] Use AccountService as account source (#8893)

* Use account service to track accounts and active account

* Remove state service active account Observables.

* Add email verified to account service

* Do not store account info on logged out accounts

* Add account activity tracking to account service

* Use last account activity from account service

* migrate or replicate account service data

* Add `AccountActivityService` that handles storing account last active data

* Move active and next active user to account service

* Remove authenticated accounts from state object

* Fold account activity into account service

* Fix builds

* Fix desktop app switch

* Fix logging out non active user

* Expand helper to handle new authenticated accounts location

* Prefer view observable to tons of async pipes

* Fix `npm run test:types`

* Correct user activity sorting test

* Be more precise about log out messaging

* Fix dev compare errors

All stored values are serializable, the next step wasn't necessary and was erroring on some types that lack `toString`.

* If the account in unlocked on load of lock component, navigate away from lock screen

* Handle no users case for auth service statuses

* Specify account to switch to

* Filter active account out of inactive accounts

* Prefer constructor init

* Improve comparator

* Use helper methods internally

* Fixup component tests

* Clarify name

* Ensure accounts object has only valid userIds

* Capitalize const values

* Prefer descriptive, single-responsibility guards

* Update libs/common/src/state-migrations/migrate.ts

Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>

* Fix merge

* Add user Id validation

activity for undefined was being set, which was resulting in requests for the auth status of `"undefined"` (string) userId, due to key enumeration. These changes stop that at both locations, as well as account add for good measure.

---------

Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
This commit is contained in:
Matt Gibson
2024-04-30 09:13:02 -04:00
committed by GitHub
parent 61d079cc34
commit c70a5aa024
67 changed files with 1380 additions and 618 deletions

View File

@@ -1,9 +1,10 @@
import { MockProxy, any, mock } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
import { BehaviorSubject, of } from "rxjs";
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
import { SearchService } from "../../abstractions/search.service";
import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
import { AccountInfo } from "../../auth/abstractions/account.service";
import { AuthService } from "../../auth/abstractions/auth.service";
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
import { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service";
@@ -13,7 +14,6 @@ import { MessagingService } from "../../platform/abstractions/messaging.service"
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils";
import { Account } from "../../platform/models/domain/account";
import { StateEventRunnerService } from "../../platform/state";
import { UserId } from "../../types/guid";
import { CipherService } from "../../vault/abstractions/cipher.service";
@@ -39,7 +39,6 @@ describe("VaultTimeoutService", () => {
let lockedCallback: jest.Mock<Promise<void>, [userId: string]>;
let loggedOutCallback: jest.Mock<Promise<void>, [expired: boolean, userId?: string]>;
let accountsSubject: BehaviorSubject<Record<string, Account>>;
let vaultTimeoutActionSubject: BehaviorSubject<VaultTimeoutAction>;
let availableVaultTimeoutActionsSubject: BehaviorSubject<VaultTimeoutAction[]>;
@@ -65,10 +64,6 @@ describe("VaultTimeoutService", () => {
lockedCallback = jest.fn();
loggedOutCallback = jest.fn();
accountsSubject = new BehaviorSubject(null);
stateService.accounts$ = accountsSubject;
vaultTimeoutActionSubject = new BehaviorSubject(VaultTimeoutAction.Lock);
vaultTimeoutSettingsService.vaultTimeoutAction$.mockReturnValue(vaultTimeoutActionSubject);
@@ -127,21 +122,39 @@ describe("VaultTimeoutService", () => {
return Promise.resolve(accounts[userId]?.vaultTimeout);
});
stateService.getLastActive.mockImplementation((options) => {
return Promise.resolve(accounts[options.userId]?.lastActive);
});
stateService.getUserId.mockResolvedValue(globalSetups?.userId);
stateService.activeAccount$ = new BehaviorSubject<string>(globalSetups?.userId);
// Set desired user active and known users on accounts service : note the only thing that matters here is that the ID are set
if (globalSetups?.userId) {
accountService.activeAccountSubject.next({
id: globalSetups.userId as UserId,
email: null,
emailVerified: false,
name: null,
});
}
accountService.accounts$ = of(
Object.entries(accounts).reduce(
(agg, [id]) => {
agg[id] = {
email: "",
emailVerified: true,
name: "",
};
return agg;
},
{} as Record<string, AccountInfo>,
),
);
accountService.accountActivity$ = of(
Object.entries(accounts).reduce(
(agg, [id, info]) => {
agg[id] = info.lastActive ? new Date(info.lastActive) : null;
return agg;
},
{} as Record<string, Date>,
),
);
platformUtilsService.isViewOpen.mockResolvedValue(globalSetups?.isViewOpen ?? false);
@@ -158,16 +171,6 @@ describe("VaultTimeoutService", () => {
],
);
});
const accountsSubjectValue: Record<string, Account> = Object.keys(accounts).reduce(
(agg, key) => {
const newPartial: Record<string, unknown> = {};
newPartial[key] = null; // No values actually matter on this other than the key
return Object.assign(agg, newPartial);
},
{} as Record<string, Account>,
);
accountsSubject.next(accountsSubjectValue);
};
const expectUserToHaveLocked = (userId: string) => {

View File

@@ -1,4 +1,4 @@
import { firstValueFrom, timeout } from "rxjs";
import { combineLatest, firstValueFrom, switchMap } from "rxjs";
import { SearchService } from "../../abstractions/search.service";
import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
@@ -64,14 +64,25 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
// Get whether or not the view is open a single time so it can be compared for each user
const isViewOpen = await this.platformUtilsService.isViewOpen();
const activeUserId = await firstValueFrom(this.stateService.activeAccount$.pipe(timeout(500)));
const accounts = await firstValueFrom(this.stateService.accounts$);
for (const userId in accounts) {
if (userId != null && (await this.shouldLock(userId, activeUserId, isViewOpen))) {
await this.executeTimeoutAction(userId);
}
}
await firstValueFrom(
combineLatest([
this.accountService.activeAccount$,
this.accountService.accountActivity$,
]).pipe(
switchMap(async ([activeAccount, accountActivity]) => {
const activeUserId = activeAccount?.id;
for (const userIdString in accountActivity) {
const userId = userIdString as UserId;
if (
userId != null &&
(await this.shouldLock(userId, accountActivity[userId], activeUserId, isViewOpen))
) {
await this.executeTimeoutAction(userId);
}
}
}),
),
);
}
async lock(userId?: string): Promise<void> {
@@ -123,6 +134,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
private async shouldLock(
userId: string,
lastActive: Date,
activeUserId: string,
isViewOpen: boolean,
): Promise<boolean> {
@@ -146,13 +158,12 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
return false;
}
const lastActive = await this.stateService.getLastActive({ userId: userId });
if (lastActive == null) {
return false;
}
const vaultTimeoutSeconds = vaultTimeout * 60;
const diffSeconds = (new Date().getTime() - lastActive) / 1000;
const diffSeconds = (new Date().getTime() - lastActive.getTime()) / 1000;
return diffSeconds >= vaultTimeoutSeconds;
}