Fold account activity into account service
This commit is contained in:
parent
06c24acc5c
commit
b12c9eeab6
|
@ -1,29 +0,0 @@
|
|||
import { AccountActivityService } from "@bitwarden/common/auth/abstractions/account-activity.service";
|
||||
import { DefaultAccountActivityService } from "@bitwarden/common/auth/services/account-activity.service";
|
||||
|
||||
import {
|
||||
FactoryOptions,
|
||||
CachedServices,
|
||||
factory,
|
||||
} from "../../../platform/background/service-factories/factory-options";
|
||||
import {
|
||||
GlobalStateProviderInitOptions,
|
||||
globalStateProviderFactory,
|
||||
} from "../../../platform/background/service-factories/global-state-provider.factory";
|
||||
|
||||
type AccountActivityServiceFactoryOptions = FactoryOptions;
|
||||
|
||||
export type AccountActivityServiceInitOptions = AccountActivityServiceFactoryOptions &
|
||||
GlobalStateProviderInitOptions;
|
||||
|
||||
export function accountActivityServiceFactory(
|
||||
cache: { accountActivityService?: AccountActivityService } & CachedServices,
|
||||
opts: AccountActivityServiceInitOptions,
|
||||
): Promise<AccountActivityService> {
|
||||
return factory(
|
||||
cache,
|
||||
"accountActivityService",
|
||||
opts,
|
||||
async () => new DefaultAccountActivityService(await globalStateProviderFactory(cache, opts)),
|
||||
);
|
||||
}
|
|
@ -19,18 +19,12 @@ import {
|
|||
messagingServiceFactory,
|
||||
} from "../../../platform/background/service-factories/messaging-service.factory";
|
||||
|
||||
import {
|
||||
AccountActivityServiceInitOptions,
|
||||
accountActivityServiceFactory,
|
||||
} from "./account-activity-service.factory";
|
||||
|
||||
type AccountServiceFactoryOptions = FactoryOptions;
|
||||
|
||||
export type AccountServiceInitOptions = AccountServiceFactoryOptions &
|
||||
MessagingServiceInitOptions &
|
||||
LogServiceInitOptions &
|
||||
GlobalStateProviderInitOptions &
|
||||
AccountActivityServiceInitOptions;
|
||||
GlobalStateProviderInitOptions;
|
||||
|
||||
export function accountServiceFactory(
|
||||
cache: { accountService?: AccountService } & CachedServices,
|
||||
|
@ -45,7 +39,6 @@ export function accountServiceFactory(
|
|||
await messagingServiceFactory(cache, opts),
|
||||
await logServiceFactory(cache, opts),
|
||||
await globalStateProviderFactory(cache, opts),
|
||||
await accountActivityServiceFactory(cache, opts),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { AccountActivityService } from "@bitwarden/common/auth/abstractions/account-activity.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import {
|
||||
|
@ -68,7 +67,6 @@ describe("ContextMenuClickedHandler", () => {
|
|||
let authService: MockProxy<AuthService>;
|
||||
let cipherService: MockProxy<CipherService>;
|
||||
let accountService: FakeAccountService;
|
||||
let accountActivityService: MockProxy<AccountActivityService>;
|
||||
let totpService: MockProxy<TotpService>;
|
||||
let eventCollectionService: MockProxy<EventCollectionService>;
|
||||
let userVerificationService: MockProxy<UserVerificationService>;
|
||||
|
@ -84,7 +82,6 @@ describe("ContextMenuClickedHandler", () => {
|
|||
accountService = mockAccountServiceWith("userId" as UserId);
|
||||
totpService = mock();
|
||||
eventCollectionService = mock();
|
||||
accountActivityService = mock();
|
||||
|
||||
sut = new ContextMenuClickedHandler(
|
||||
copyToClipboard,
|
||||
|
@ -96,7 +93,6 @@ describe("ContextMenuClickedHandler", () => {
|
|||
eventCollectionService,
|
||||
userVerificationService,
|
||||
accountService,
|
||||
accountActivityService,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { AccountActivityService } from "@bitwarden/common/auth/abstractions/account-activity.service";
|
||||
import { 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";
|
||||
|
@ -29,7 +28,6 @@ import { CipherType } from "@bitwarden/common/vault/enums";
|
|||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import { accountActivityServiceFactory } from "../../auth/background/service-factories/account-activity-service.factory";
|
||||
import { accountServiceFactory } from "../../auth/background/service-factories/account-service.factory";
|
||||
import {
|
||||
authServiceFactory,
|
||||
|
@ -79,7 +77,6 @@ export class ContextMenuClickedHandler {
|
|||
private eventCollectionService: EventCollectionService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
private accountService: AccountService,
|
||||
private accountActivityService: AccountActivityService,
|
||||
) {}
|
||||
|
||||
static async mv3Create(cachedServices: CachedServices) {
|
||||
|
@ -137,7 +134,6 @@ export class ContextMenuClickedHandler {
|
|||
await eventCollectionServiceFactory(cachedServices, serviceOptions),
|
||||
await userVerificationServiceFactory(cachedServices, serviceOptions),
|
||||
await accountServiceFactory(cachedServices, serviceOptions),
|
||||
await accountActivityServiceFactory(cachedServices, serviceOptions),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -248,7 +244,7 @@ export class ContextMenuClickedHandler {
|
|||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
await this.accountActivityService.setAccountActivity(activeUserId, new Date());
|
||||
await this.accountService.setAccountActivity(activeUserId, new Date());
|
||||
switch (info.parentMenuItemId) {
|
||||
case AUTOFILL_ID:
|
||||
case AUTOFILL_IDENTITY_ID:
|
||||
|
|
|
@ -27,7 +27,6 @@ import { OrganizationService } from "@bitwarden/common/admin-console/services/or
|
|||
import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
|
||||
import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service";
|
||||
import { AccountActivityService } from "@bitwarden/common/auth/abstractions/account-activity.service";
|
||||
import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service";
|
||||
|
@ -43,7 +42,6 @@ import { UserVerificationApiServiceAbstraction } from "@bitwarden/common/auth/ab
|
|||
import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { DefaultAccountActivityService } from "@bitwarden/common/auth/services/account-activity.service";
|
||||
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
||||
import { AvatarService } from "@bitwarden/common/auth/services/avatar.service";
|
||||
|
@ -323,7 +321,6 @@ export default class MainBackground {
|
|||
deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction;
|
||||
authRequestService: AuthRequestServiceAbstraction;
|
||||
accountService: AccountServiceAbstraction;
|
||||
accountActivityService: AccountActivityService;
|
||||
globalStateProvider: GlobalStateProvider;
|
||||
pinCryptoService: PinCryptoServiceAbstraction;
|
||||
singleUserStateProvider: SingleUserStateProvider;
|
||||
|
@ -487,12 +484,10 @@ export default class MainBackground {
|
|||
storageServiceProvider,
|
||||
stateEventRegistrarService,
|
||||
);
|
||||
this.accountActivityService = new DefaultAccountActivityService(this.globalStateProvider);
|
||||
this.accountService = new AccountServiceImplementation(
|
||||
this.messagingService,
|
||||
this.logService,
|
||||
this.globalStateProvider,
|
||||
this.accountActivityService,
|
||||
);
|
||||
this.activeUserStateProvider = new DefaultActiveUserStateProvider(
|
||||
this.accountService,
|
||||
|
@ -772,7 +767,6 @@ export default class MainBackground {
|
|||
this.authService,
|
||||
this.vaultTimeoutSettingsService,
|
||||
this.stateEventRunnerService,
|
||||
this.accountActivityService,
|
||||
lockedCallback,
|
||||
logoutCallback,
|
||||
);
|
||||
|
@ -957,7 +951,6 @@ export default class MainBackground {
|
|||
this.fido2Background,
|
||||
messageListener,
|
||||
this.accountService,
|
||||
this.accountActivityService,
|
||||
);
|
||||
this.nativeMessagingBackground = new NativeMessagingBackground(
|
||||
this.accountService,
|
||||
|
@ -1051,7 +1044,6 @@ export default class MainBackground {
|
|||
this.eventCollectionService,
|
||||
this.userVerificationService,
|
||||
this.accountService,
|
||||
this.accountActivityService,
|
||||
);
|
||||
|
||||
this.contextMenusBackground = new ContextMenusBackground(contextMenuClickedHandler);
|
||||
|
@ -1258,7 +1250,7 @@ export default class MainBackground {
|
|||
const needStorageReseed = await this.needsStorageReseed();
|
||||
|
||||
const newActiveUser = await firstValueFrom(
|
||||
this.accountService.nextUpActiveUsers$.pipe(map((users) => users[0]?.id ?? null)),
|
||||
this.accountService.nextUpAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
await this.stateService.clean({ userId: userId });
|
||||
await this.accountService.clean(userId);
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { firstValueFrom, map, mergeMap } from "rxjs";
|
||||
|
||||
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
||||
import { AccountActivityService } from "@bitwarden/common/auth/abstractions/account-activity.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
|
||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
|
@ -47,7 +46,6 @@ export default class RuntimeBackground {
|
|||
private fido2Background: Fido2Background,
|
||||
private messageListener: MessageListener,
|
||||
private accountService: AccountService,
|
||||
private accountActivityService: AccountActivityService,
|
||||
) {
|
||||
// onInstalled listener must be wired up before anything else, so we do it in the ctor
|
||||
chrome.runtime.onInstalled.addListener((details: any) => {
|
||||
|
@ -112,7 +110,7 @@ export default class RuntimeBackground {
|
|||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
await this.accountActivityService.setAccountActivity(activeUserId, new Date());
|
||||
await this.accountService.setAccountActivity(activeUserId, new Date());
|
||||
const totpCode = await this.autofillService.doAutoFillActiveTab(
|
||||
[
|
||||
{
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
import { VaultTimeoutService as AbstractVaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||
|
||||
import {
|
||||
accountActivityServiceFactory,
|
||||
AccountActivityServiceInitOptions,
|
||||
} from "../../auth/background/service-factories/account-activity-service.factory";
|
||||
import {
|
||||
accountServiceFactory,
|
||||
AccountServiceInitOptions,
|
||||
|
@ -81,8 +77,7 @@ export type VaultTimeoutServiceInitOptions = VaultTimeoutServiceFactoryOptions &
|
|||
StateServiceInitOptions &
|
||||
AuthServiceInitOptions &
|
||||
VaultTimeoutSettingsServiceInitOptions &
|
||||
StateEventRunnerServiceInitOptions &
|
||||
AccountActivityServiceInitOptions;
|
||||
StateEventRunnerServiceInitOptions;
|
||||
|
||||
export function vaultTimeoutServiceFactory(
|
||||
cache: { vaultTimeoutService?: AbstractVaultTimeoutService } & CachedServices,
|
||||
|
@ -107,7 +102,6 @@ export function vaultTimeoutServiceFactory(
|
|||
await authServiceFactory(cache, opts),
|
||||
await vaultTimeoutSettingsServiceFactory(cache, opts),
|
||||
await stateEventRunnerServiceFactory(cache, opts),
|
||||
await accountActivityServiceFactory(cache, opts),
|
||||
opts.vaultTimeoutServiceOptions.lockedCallback,
|
||||
opts.vaultTimeoutServiceOptions.loggedOutCallback,
|
||||
),
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import { Component, Input } from "@angular/core";
|
||||
import { Observable, combineLatest, map, of, switchMap } from "rxjs";
|
||||
import { Observable, map, of, switchMap } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { enableAccountSwitching } from "../flags";
|
||||
|
||||
|
@ -16,18 +14,15 @@ export class HeaderComponent {
|
|||
@Input() noTheme = false;
|
||||
@Input() hideAccountSwitcher = false;
|
||||
authedAccounts$: Observable<boolean>;
|
||||
constructor(accountService: AccountService, authService: AuthService) {
|
||||
this.authedAccounts$ = accountService.accounts$.pipe(
|
||||
switchMap((accounts) => {
|
||||
constructor(authService: AuthService) {
|
||||
this.authedAccounts$ = authService.authStatuses$.pipe(
|
||||
map((record) => Object.values(record)),
|
||||
switchMap((statuses) => {
|
||||
if (!enableAccountSwitching()) {
|
||||
return of(false);
|
||||
}
|
||||
|
||||
return combineLatest(
|
||||
Object.keys(accounts).map((id) => authService.authStatusFor$(id as UserId)),
|
||||
).pipe(
|
||||
map((statuses) => statuses.some((status) => status !== AuthenticationStatus.LoggedOut)),
|
||||
);
|
||||
return of(statuses.some((status) => status !== AuthenticationStatus.LoggedOut));
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -193,26 +193,33 @@ export class LocalBackedSessionStorageService
|
|||
}
|
||||
|
||||
private compareValues<T>(value1: T, value2: T): boolean {
|
||||
if (value1 == null && value2 == null) {
|
||||
try {
|
||||
if (value1 == null && value2 == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value1 && value2 == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value1 == null && value2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof value1 !== "object" || typeof value2 !== "object") {
|
||||
return value1 === value2;
|
||||
}
|
||||
|
||||
if (JSON.stringify(value1) === JSON.stringify(value2)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return Object.entries(value1).sort().toString() === Object.entries(value2).sort().toString();
|
||||
} catch (e) {
|
||||
this.logService.error(
|
||||
`error comparing values\n${JSON.stringify(value1)}\n${JSON.stringify(value2)}`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value1 && value2 == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value1 == null && value2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof value1 !== "object" || typeof value2 !== "object") {
|
||||
return value1 === value2;
|
||||
}
|
||||
|
||||
if (JSON.stringify(value1) === JSON.stringify(value2)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return Object.entries(value1).sort().toString() === Object.entries(value2).sort().toString();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angula
|
|||
import { NavigationEnd, Router, RouterOutlet } from "@angular/router";
|
||||
import { Subject, takeUntil, firstValueFrom, concatMap, filter, tap } from "rxjs";
|
||||
|
||||
import { AccountActivityService } from "@bitwarden/common/auth/abstractions/account-activity.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";
|
||||
|
@ -51,7 +50,6 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
private messageListener: MessageListener,
|
||||
private toastService: ToastService,
|
||||
private accountService: AccountService,
|
||||
private accountActivityService: AccountActivityService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
|
@ -211,7 +209,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
this.lastActivity = now;
|
||||
await this.accountActivityService.setAccountActivity(this.activeUserId, now);
|
||||
await this.accountService.setAccountActivity(this.activeUserId, now);
|
||||
}
|
||||
|
||||
private showToast(msg: any) {
|
||||
|
|
|
@ -20,7 +20,6 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaul
|
|||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
||||
import { AccountActivityService } from "@bitwarden/common/auth/abstractions/account-activity.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
|
@ -152,7 +151,6 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
private stateEventRunnerService: StateEventRunnerService,
|
||||
private providerService: ProviderService,
|
||||
private accountService: AccountService,
|
||||
private accountActivityService: AccountActivityService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
|
@ -634,7 +632,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
this.lastActivity = now;
|
||||
await this.accountActivityService.setAccountActivity(this.activeUserId, now);
|
||||
await this.accountService.setAccountActivity(this.activeUserId, now);
|
||||
|
||||
// Idle states
|
||||
if (this.isIdle) {
|
||||
|
|
|
@ -10,7 +10,6 @@ import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
|||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||
import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountActivityService } from "@bitwarden/common/auth/abstractions/account-activity.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
|
@ -89,7 +88,6 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||
private paymentMethodWarningService: PaymentMethodWarningService,
|
||||
private organizationService: InternalOrganizationServiceAbstraction,
|
||||
private accountService: AccountService,
|
||||
private accountActivityService: AccountActivityService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
|
@ -311,7 +309,7 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||
}
|
||||
|
||||
this.lastActivity = now;
|
||||
await this.accountActivityService.setAccountActivity(activeUserId, now);
|
||||
await this.accountService.setAccountActivity(activeUserId, now);
|
||||
// Idle states
|
||||
if (this.isIdle) {
|
||||
this.isIdle = false;
|
||||
|
|
|
@ -51,7 +51,6 @@ import { PolicyApiService } from "@bitwarden/common/admin-console/services/polic
|
|||
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
|
||||
import { ProviderApiService } from "@bitwarden/common/admin-console/services/provider/provider-api.service";
|
||||
import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service";
|
||||
import { AccountActivityService } from "@bitwarden/common/auth/abstractions/account-activity.service";
|
||||
import { AccountApiService as AccountApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/account-api.service";
|
||||
import {
|
||||
AccountService,
|
||||
|
@ -78,7 +77,6 @@ import { UserVerificationService as UserVerificationServiceAbstraction } from "@
|
|||
import { WebAuthnLoginApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login-api.service.abstraction";
|
||||
import { WebAuthnLoginPrfCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login-prf-crypto.service.abstraction";
|
||||
import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction";
|
||||
import { DefaultAccountActivityService } from "@bitwarden/common/auth/services/account-activity.service";
|
||||
import { AccountApiServiceImplementation } from "@bitwarden/common/auth/services/account-api.service";
|
||||
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
||||
import { AnonymousHubService } from "@bitwarden/common/auth/services/anonymous-hub.service";
|
||||
|
@ -483,7 +481,7 @@ const safeProviders: SafeProvider[] = [
|
|||
safeProvider({
|
||||
provide: InternalAccountService,
|
||||
useClass: AccountServiceImplementation,
|
||||
deps: [MessagingServiceAbstraction, LogService, GlobalStateProvider, AccountActivityService],
|
||||
deps: [MessagingServiceAbstraction, LogService, GlobalStateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AccountServiceAbstraction,
|
||||
|
@ -666,7 +664,6 @@ const safeProviders: SafeProvider[] = [
|
|||
AuthServiceAbstraction,
|
||||
VaultTimeoutSettingsServiceAbstraction,
|
||||
StateEventRunnerService,
|
||||
AccountActivityService,
|
||||
LOCKED_CALLBACK,
|
||||
LOGOUT_CALLBACK,
|
||||
],
|
||||
|
@ -1159,11 +1156,6 @@ const safeProviders: SafeProvider[] = [
|
|||
useClass: ProviderApiService,
|
||||
deps: [ApiServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AccountActivityService,
|
||||
useClass: DefaultAccountActivityService,
|
||||
deps: [GlobalStateProvider],
|
||||
}),
|
||||
];
|
||||
|
||||
function encryptServiceFactory(
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { mock } from "jest-mock-extended";
|
||||
import { Observable, ReplaySubject } from "rxjs";
|
||||
import { ReplaySubject, combineLatest, map } from "rxjs";
|
||||
|
||||
import { AccountInfo, AccountService } from "../src/auth/abstractions/account.service";
|
||||
import { UserId } from "../src/types/guid";
|
||||
|
@ -33,14 +33,31 @@ export class FakeAccountService implements AccountService {
|
|||
activeAccountSubject = new ReplaySubject<{ id: UserId } & AccountInfo>(1);
|
||||
// eslint-disable-next-line rxjs/no-exposed-subjects -- test class
|
||||
accountActivitySubject = new ReplaySubject<Record<UserId, Date>>(1);
|
||||
// eslint-disable-next-line rxjs/no-exposed-subjects -- test class
|
||||
nextUpActiveUsers$: Observable<({ id: UserId } & AccountInfo)[]>;
|
||||
private _activeUserId: UserId;
|
||||
get activeUserId() {
|
||||
return this._activeUserId;
|
||||
}
|
||||
accounts$ = this.accountsSubject.asObservable();
|
||||
activeAccount$ = this.activeAccountSubject.asObservable();
|
||||
accountActivity$ = this.accountActivitySubject.asObservable();
|
||||
get sortedUserIds$() {
|
||||
return this.accountActivity$.pipe(
|
||||
map((activity) => {
|
||||
return Object.entries(activity)
|
||||
.map(([userId, lastActive]: [UserId, Date]) => ({ userId, lastActive }))
|
||||
.sort((a, b) => a.lastActive.getTime() - b.lastActive.getTime())
|
||||
.map((a) => a.userId);
|
||||
}),
|
||||
);
|
||||
}
|
||||
get nextUpAccount$() {
|
||||
return combineLatest([this.accounts$, this.activeAccount$, this.sortedUserIds$]).pipe(
|
||||
map(([accounts, activeAccount, sortedUserIds]) => {
|
||||
const nextId = sortedUserIds.find((id) => id !== activeAccount?.id && accounts[id] != null);
|
||||
return nextId ? { id: nextId, ...accounts[nextId] } : null;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
constructor(initialData: Record<UserId, AccountInfo>, accountActivity?: Record<UserId, Date>) {
|
||||
this.accountsSubject.next(initialData);
|
||||
|
@ -48,6 +65,13 @@ export class FakeAccountService implements AccountService {
|
|||
this.activeAccountSubject.next(null);
|
||||
this.accountActivitySubject.next(accountActivity);
|
||||
}
|
||||
setAccountActivity(userId: UserId, lastActivity: Date): Promise<void> {
|
||||
this.accountActivitySubject.next({
|
||||
...this.accountActivitySubject["_buffer"][0],
|
||||
[userId]: lastActivity,
|
||||
});
|
||||
return this.mock.setAccountActivity(userId, lastActivity);
|
||||
}
|
||||
|
||||
async addAccount(userId: UserId, accountData: AccountInfo): Promise<void> {
|
||||
const current = this.accountsSubject["_buffer"][0] ?? {};
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
import { Observable } from "rxjs";
|
||||
|
||||
import { UserId } from "../../types/guid";
|
||||
|
||||
export abstract class AccountActivityService {
|
||||
/**
|
||||
* Observable of the last activity time for each account.
|
||||
*/
|
||||
accountActivity$: Observable<Record<UserId, Date>>;
|
||||
/**
|
||||
* Observable of the next-most-recent active account.
|
||||
*/
|
||||
sortedUserIds$: Observable<UserId[]>;
|
||||
|
||||
/**
|
||||
* Updates the given user's last activity time.
|
||||
* @param userId
|
||||
* @param lastActivity
|
||||
*/
|
||||
abstract setAccountActivity(userId: UserId, lastActivity: Date): Promise<void>;
|
||||
/**
|
||||
* Removes a user from the account activity list.
|
||||
* @param userId
|
||||
*/
|
||||
abstract removeAccountActivity(userId: UserId): Promise<void>;
|
||||
|
||||
/**
|
||||
* Returns the most recent account that is not the current active account, as expressed by the `currentUser` observable.
|
||||
* @param currentUser Observable of the current active account's UserId.
|
||||
*/
|
||||
abstract nextUpActiveAccount(currentUser: Observable<UserId>): Observable<UserId | null>;
|
||||
}
|
|
@ -30,8 +30,15 @@ export function accountInfoEqual(a: AccountInfo, b: AccountInfo) {
|
|||
export abstract class AccountService {
|
||||
accounts$: Observable<Record<UserId, AccountInfo>>;
|
||||
activeAccount$: Observable<{ id: UserId | undefined } & AccountInfo>;
|
||||
/** Ordered list of next accounts, sorted in descending order by most recent account activity */
|
||||
nextUpActiveUsers$: Observable<({ id: UserId } & AccountInfo)[]>;
|
||||
|
||||
/**
|
||||
* Observable of the last activity time for each account.
|
||||
*/
|
||||
accountActivity$: Observable<Record<UserId, Date>>;
|
||||
/** Account list in order of descending recency */
|
||||
sortedUserIds$: Observable<UserId[]>;
|
||||
/** Next account that is not the current active account */
|
||||
nextUpAccount$: Observable<{ id: UserId } & AccountInfo>;
|
||||
/**
|
||||
* Updates the `accounts$` observable with the new account data.
|
||||
*
|
||||
|
@ -70,6 +77,12 @@ export abstract class AccountService {
|
|||
* @param userId
|
||||
*/
|
||||
abstract clean(userId: UserId): Promise<void>;
|
||||
/**
|
||||
* Updates the given user's last activity time.
|
||||
* @param userId
|
||||
* @param lastActivity
|
||||
*/
|
||||
abstract setAccountActivity(userId: UserId, lastActivity: Date): Promise<void>;
|
||||
}
|
||||
|
||||
export abstract class InternalAccountService extends AccountService {
|
||||
|
|
|
@ -1,179 +0,0 @@
|
|||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { FakeGlobalState, FakeGlobalStateProvider } from "../../../spec";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { AccountActivityService } from "../abstractions/account-activity.service";
|
||||
|
||||
import { ACCOUNT_ACTIVITY, DefaultAccountActivityService } from "./account-activity.service";
|
||||
|
||||
describe("AccountActivityService", () => {
|
||||
let globalStateProvider: FakeGlobalStateProvider;
|
||||
let state: FakeGlobalState<Record<UserId, Date>>;
|
||||
|
||||
let sut: AccountActivityService;
|
||||
|
||||
beforeEach(() => {
|
||||
globalStateProvider = new FakeGlobalStateProvider();
|
||||
state = globalStateProvider.getFake(ACCOUNT_ACTIVITY);
|
||||
|
||||
sut = new DefaultAccountActivityService(globalStateProvider);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("accountActivity$", () => {
|
||||
it("returns the account activity state", async () => {
|
||||
state.stateSubject.next({
|
||||
[toId("user1")]: new Date(1),
|
||||
[toId("user2")]: new Date(2),
|
||||
});
|
||||
|
||||
await expect(firstValueFrom(sut.accountActivity$)).resolves.toEqual({
|
||||
[toId("user1")]: new Date(1),
|
||||
[toId("user2")]: new Date(2),
|
||||
});
|
||||
});
|
||||
|
||||
it("returns an empty object when account activity is null", async () => {
|
||||
state.stateSubject.next(null);
|
||||
|
||||
await expect(firstValueFrom(sut.accountActivity$)).resolves.toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe("sortedUserIds$", () => {
|
||||
it("returns the sorted user ids by date", async () => {
|
||||
state.stateSubject.next({
|
||||
[toId("user1")]: new Date(3),
|
||||
[toId("user2")]: new Date(2),
|
||||
[toId("user3")]: new Date(1),
|
||||
});
|
||||
|
||||
await expect(firstValueFrom(sut.sortedUserIds$)).resolves.toEqual([
|
||||
"user3" as UserId,
|
||||
"user2" as UserId,
|
||||
"user1" as UserId,
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns an empty array when account activity is null", async () => {
|
||||
state.stateSubject.next(null);
|
||||
|
||||
await expect(firstValueFrom(sut.sortedUserIds$)).resolves.toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("nextUpActiveAccount", () => {
|
||||
beforeEach(() => {
|
||||
state.stateSubject.next({
|
||||
[toId("user1")]: new Date(1),
|
||||
[toId("user2")]: new Date(2),
|
||||
[toId("user3")]: new Date(3),
|
||||
});
|
||||
});
|
||||
|
||||
it("returns returns the next most recent account when active is most recent", async () => {
|
||||
const currentUser = of("user1" as UserId);
|
||||
|
||||
await expect(firstValueFrom(sut.nextUpActiveAccount(currentUser))).resolves.toBe(
|
||||
"user2" as UserId,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns most recent account when active is not most recent", async () => {
|
||||
const currentUser = of("user2" as UserId);
|
||||
|
||||
await expect(firstValueFrom(sut.nextUpActiveAccount(currentUser))).resolves.toBe(
|
||||
"user1" as UserId,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns the most recent account when active is null", async () => {
|
||||
const currentUser = of(null);
|
||||
|
||||
await expect(firstValueFrom(sut.nextUpActiveAccount(currentUser))).resolves.toBe(
|
||||
"user1" as UserId,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns null when there are no accounts", async () => {
|
||||
state.stateSubject.next({});
|
||||
|
||||
const currentUser = of(null);
|
||||
|
||||
await expect(firstValueFrom(sut.nextUpActiveAccount(currentUser))).resolves.toBe(null);
|
||||
});
|
||||
|
||||
it("returns null when there are no accounts but there is an active account", async () => {
|
||||
state.stateSubject.next({});
|
||||
|
||||
const currentUser = of("user1" as UserId);
|
||||
|
||||
await expect(firstValueFrom(sut.nextUpActiveAccount(currentUser))).resolves.toBe(null);
|
||||
});
|
||||
|
||||
it("returns null when account activity is null", async () => {
|
||||
state.stateSubject.next(null);
|
||||
|
||||
const currentUser = of(null);
|
||||
|
||||
await expect(firstValueFrom(sut.nextUpActiveAccount(currentUser))).resolves.toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setAccountActivity", () => {
|
||||
it("sets the account activity", async () => {
|
||||
await sut.setAccountActivity("user1" as UserId, new Date(1));
|
||||
|
||||
expect(state.nextMock).toHaveBeenCalledWith({ user1: new Date(1) });
|
||||
});
|
||||
|
||||
it("does not update if the activity is the same", async () => {
|
||||
state.stateSubject.next({ [toId("user1")]: new Date(1) });
|
||||
|
||||
await sut.setAccountActivity("user1" as UserId, new Date(1));
|
||||
|
||||
expect(state.nextMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeAccountActivity", () => {
|
||||
it("removes the account activity", async () => {
|
||||
state.stateSubject.next({ [toId("user1")]: new Date(1) });
|
||||
|
||||
await sut.removeAccountActivity("user1" as UserId);
|
||||
|
||||
expect(state.nextMock).toHaveBeenCalledWith({});
|
||||
});
|
||||
|
||||
it("does not update if the account activity is null", async () => {
|
||||
state.stateSubject.next(null);
|
||||
|
||||
await sut.removeAccountActivity("user1" as UserId);
|
||||
|
||||
expect(state.nextMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not update if the account activity is empty", async () => {
|
||||
state.stateSubject.next({});
|
||||
|
||||
await sut.removeAccountActivity("user1" as UserId);
|
||||
|
||||
expect(state.nextMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not update if the account activity does not contain the user", async () => {
|
||||
state.stateSubject.next({ [toId("user2")]: new Date(1) });
|
||||
|
||||
await sut.removeAccountActivity("user1" as UserId);
|
||||
|
||||
expect(state.nextMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function toId(userId: string) {
|
||||
return userId as UserId;
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
import { Observable, combineLatest, map } from "rxjs";
|
||||
|
||||
import { ACCOUNT_DISK, GlobalStateProvider, KeyDefinition } from "../../platform/state";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { AccountActivityService } from "../abstractions/account-activity.service";
|
||||
|
||||
export const ACCOUNT_ACTIVITY = KeyDefinition.record<Date, UserId>(ACCOUNT_DISK, "activity", {
|
||||
deserializer: (activity) => new Date(activity),
|
||||
});
|
||||
|
||||
export class DefaultAccountActivityService implements AccountActivityService {
|
||||
accountActivity$: Observable<Record<UserId, Date>>;
|
||||
sortedUserIds$: Observable<UserId[]>;
|
||||
|
||||
constructor(private globalStateProvider: GlobalStateProvider) {
|
||||
this.accountActivity$ = this.globalStateProvider
|
||||
.get(ACCOUNT_ACTIVITY)
|
||||
.state$.pipe(map((activity) => activity ?? {}));
|
||||
this.sortedUserIds$ = this.accountActivity$.pipe(
|
||||
map((activity) => {
|
||||
return Object.entries(activity)
|
||||
.map(([userId, lastActive]: [UserId, Date]) => ({ userId, lastActive }))
|
||||
.sort((a, b) => a.lastActive.getTime() - b.lastActive.getTime())
|
||||
.map((a) => a.userId);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
nextUpActiveAccount(currentUser: Observable<UserId>): Observable<UserId | null> {
|
||||
return combineLatest([this.sortedUserIds$, currentUser]).pipe(
|
||||
map(([sortedUserIds, currentUserId]) => {
|
||||
const filtered = sortedUserIds.filter((userId) => userId !== currentUserId);
|
||||
if (filtered.length > 0) {
|
||||
return filtered[0];
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async setAccountActivity(userId: UserId, lastActivity: Date): Promise<void> {
|
||||
await this.globalStateProvider.get(ACCOUNT_ACTIVITY).update(
|
||||
(activity) => {
|
||||
activity ||= {};
|
||||
activity[userId] = lastActivity;
|
||||
return activity;
|
||||
},
|
||||
{
|
||||
shouldUpdate: (oldActivity) => oldActivity?.[userId]?.getTime() !== lastActivity?.getTime(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async removeAccountActivity(userId: UserId): Promise<void> {
|
||||
await this.globalStateProvider.get(ACCOUNT_ACTIVITY).update(
|
||||
(activity) => {
|
||||
if (activity == null) {
|
||||
return activity;
|
||||
}
|
||||
delete activity[userId];
|
||||
return activity;
|
||||
},
|
||||
{ shouldUpdate: (oldActivity) => oldActivity?.[userId] != null },
|
||||
);
|
||||
}
|
||||
}
|
|
@ -12,12 +12,12 @@ import { trackEmissions } from "../../../spec/utils";
|
|||
import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { MessagingService } from "../../platform/abstractions/messaging.service";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { AccountActivityService } from "../abstractions/account-activity.service";
|
||||
import { AccountInfo, accountInfoEqual } from "../abstractions/account.service";
|
||||
|
||||
import {
|
||||
ACCOUNT_ACCOUNTS,
|
||||
ACCOUNT_ACTIVE_ACCOUNT_ID,
|
||||
ACCOUNT_ACTIVITY,
|
||||
AccountServiceImplementation,
|
||||
} from "./account.service";
|
||||
|
||||
|
@ -64,7 +64,6 @@ describe("accountInfoEqual", () => {
|
|||
describe("accountService", () => {
|
||||
let messagingService: MockProxy<MessagingService>;
|
||||
let logService: MockProxy<LogService>;
|
||||
let accountActivityService: MockProxy<AccountActivityService>;
|
||||
let globalStateProvider: FakeGlobalStateProvider;
|
||||
let sut: AccountServiceImplementation;
|
||||
let accountsState: FakeGlobalState<Record<UserId, AccountInfo>>;
|
||||
|
@ -75,15 +74,9 @@ describe("accountService", () => {
|
|||
beforeEach(() => {
|
||||
messagingService = mock();
|
||||
logService = mock();
|
||||
accountActivityService = mock();
|
||||
globalStateProvider = new FakeGlobalStateProvider();
|
||||
|
||||
sut = new AccountServiceImplementation(
|
||||
messagingService,
|
||||
logService,
|
||||
globalStateProvider,
|
||||
accountActivityService,
|
||||
);
|
||||
sut = new AccountServiceImplementation(messagingService, logService, globalStateProvider);
|
||||
|
||||
accountsState = globalStateProvider.getFake(ACCOUNT_ACCOUNTS);
|
||||
activeAccountIdState = globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID);
|
||||
|
@ -141,12 +134,11 @@ describe("accountService", () => {
|
|||
});
|
||||
|
||||
it("sets the last active date of the account to now", async () => {
|
||||
const state = globalStateProvider.getFake(ACCOUNT_ACTIVITY);
|
||||
state.stateSubject.next({});
|
||||
await sut.addAccount(userId, userInfo);
|
||||
|
||||
expect(accountActivityService.setAccountActivity).toHaveBeenCalledWith(
|
||||
userId,
|
||||
expect.any(Date),
|
||||
);
|
||||
expect(state.nextMock).toHaveBeenCalledWith({ userId: expect.any(Date) });
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -239,9 +231,12 @@ describe("accountService", () => {
|
|||
});
|
||||
|
||||
it("removes account activity of the given user", async () => {
|
||||
const state = globalStateProvider.getFake(ACCOUNT_ACTIVITY);
|
||||
state.stateSubject.next({ [userId]: new Date() });
|
||||
|
||||
await sut.clean(userId);
|
||||
|
||||
expect(accountActivityService.removeAccountActivity).toHaveBeenCalledWith(userId);
|
||||
expect(state.nextMock).toHaveBeenCalledWith({});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -263,4 +258,73 @@ describe("accountService", () => {
|
|||
expect(sut.switchAccount("unknown" as UserId)).rejects.toThrowError("Account does not exist");
|
||||
});
|
||||
});
|
||||
|
||||
describe("account activity", () => {
|
||||
let state: FakeGlobalState<Record<UserId, Date>>;
|
||||
|
||||
beforeEach(() => {
|
||||
state = globalStateProvider.getFake(ACCOUNT_ACTIVITY);
|
||||
});
|
||||
describe("accountActivity$", () => {
|
||||
it("returns the account activity state", async () => {
|
||||
state.stateSubject.next({
|
||||
[toId("user1")]: new Date(1),
|
||||
[toId("user2")]: new Date(2),
|
||||
});
|
||||
|
||||
await expect(firstValueFrom(sut.accountActivity$)).resolves.toEqual({
|
||||
[toId("user1")]: new Date(1),
|
||||
[toId("user2")]: new Date(2),
|
||||
});
|
||||
});
|
||||
|
||||
it("returns an empty object when account activity is null", async () => {
|
||||
state.stateSubject.next(null);
|
||||
|
||||
await expect(firstValueFrom(sut.accountActivity$)).resolves.toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe("sortedUserIds$", () => {
|
||||
it("returns the sorted user ids by date", async () => {
|
||||
state.stateSubject.next({
|
||||
[toId("user1")]: new Date(3),
|
||||
[toId("user2")]: new Date(2),
|
||||
[toId("user3")]: new Date(1),
|
||||
});
|
||||
|
||||
await expect(firstValueFrom(sut.sortedUserIds$)).resolves.toEqual([
|
||||
"user3" as UserId,
|
||||
"user2" as UserId,
|
||||
"user1" as UserId,
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns an empty array when account activity is null", async () => {
|
||||
state.stateSubject.next(null);
|
||||
|
||||
await expect(firstValueFrom(sut.sortedUserIds$)).resolves.toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setAccountActivity", () => {
|
||||
it("sets the account activity", async () => {
|
||||
await sut.setAccountActivity("user1" as UserId, new Date(1));
|
||||
|
||||
expect(state.nextMock).toHaveBeenCalledWith({ user1: new Date(1) });
|
||||
});
|
||||
|
||||
it("does not update if the activity is the same", async () => {
|
||||
state.stateSubject.next({ [toId("user1")]: new Date(1) });
|
||||
|
||||
await sut.setAccountActivity("user1" as UserId, new Date(1));
|
||||
|
||||
expect(state.nextMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function toId(userId: string) {
|
||||
return userId as UserId;
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ import {
|
|||
KeyDefinition,
|
||||
} from "../../platform/state";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { AccountActivityService } from "../abstractions/account-activity.service";
|
||||
|
||||
export const ACCOUNT_ACCOUNTS = KeyDefinition.record<AccountInfo, UserId>(
|
||||
ACCOUNT_DISK,
|
||||
|
@ -44,13 +43,14 @@ export class AccountServiceImplementation implements InternalAccountService {
|
|||
|
||||
accounts$;
|
||||
activeAccount$;
|
||||
nextUpActiveUsers$;
|
||||
accountActivity$;
|
||||
sortedUserIds$;
|
||||
nextUpAccount$;
|
||||
|
||||
constructor(
|
||||
private messagingService: MessagingService,
|
||||
private logService: LogService,
|
||||
private globalStateProvider: GlobalStateProvider,
|
||||
private accountActivityService: AccountActivityService,
|
||||
) {
|
||||
this.accountsState = this.globalStateProvider.get(ACCOUNT_ACCOUNTS);
|
||||
this.activeAccountIdState = this.globalStateProvider.get(ACCOUNT_ACTIVE_ACCOUNT_ID);
|
||||
|
@ -64,15 +64,25 @@ export class AccountServiceImplementation implements InternalAccountService {
|
|||
distinctUntilChanged((a, b) => a?.id === b?.id && accountInfoEqual(a, b)),
|
||||
shareReplay({ bufferSize: 1, refCount: false }),
|
||||
);
|
||||
this.nextUpActiveUsers$ = combineLatest([
|
||||
this.accountActivity$ = this.globalStateProvider
|
||||
.get(ACCOUNT_ACTIVITY)
|
||||
.state$.pipe(map((activity) => activity ?? {}));
|
||||
this.sortedUserIds$ = this.accountActivity$.pipe(
|
||||
map((activity) => {
|
||||
return Object.entries(activity)
|
||||
.map(([userId, lastActive]: [UserId, Date]) => ({ userId, lastActive }))
|
||||
.sort((a, b) => b.lastActive.getTime() - a.lastActive.getTime()) // later dates first
|
||||
.map((a) => a.userId);
|
||||
}),
|
||||
);
|
||||
this.nextUpAccount$ = combineLatest([
|
||||
this.accounts$,
|
||||
this.activeAccount$,
|
||||
this.accountActivityService.sortedUserIds$,
|
||||
this.sortedUserIds$,
|
||||
]).pipe(
|
||||
map(([accounts, activeAccount, sortedUserIds]) => {
|
||||
return sortedUserIds
|
||||
.filter((id) => id !== activeAccount?.id && accounts[id] != null)
|
||||
.map((id) => ({ id, ...accounts[id] }));
|
||||
const nextId = sortedUserIds.find((id) => id !== activeAccount?.id && accounts[id] != null);
|
||||
return nextId ? { id: nextId, ...accounts[nextId] } : null;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
@ -83,7 +93,7 @@ export class AccountServiceImplementation implements InternalAccountService {
|
|||
accounts[userId] = accountData;
|
||||
return accounts;
|
||||
});
|
||||
await this.accountActivityService.setAccountActivity(userId, new Date());
|
||||
await this.setAccountActivity(userId, new Date());
|
||||
}
|
||||
|
||||
async setAccountName(userId: UserId, name: string): Promise<void> {
|
||||
|
@ -100,7 +110,7 @@ export class AccountServiceImplementation implements InternalAccountService {
|
|||
|
||||
async clean(userId: UserId) {
|
||||
await this.setAccountInfo(userId, loggedOutInfo);
|
||||
await this.accountActivityService.removeAccountActivity(userId);
|
||||
await this.removeAccountActivity(userId);
|
||||
}
|
||||
|
||||
async switchAccount(userId: UserId): Promise<void> {
|
||||
|
@ -126,6 +136,32 @@ export class AccountServiceImplementation implements InternalAccountService {
|
|||
);
|
||||
}
|
||||
|
||||
async setAccountActivity(userId: UserId, lastActivity: Date): Promise<void> {
|
||||
await this.globalStateProvider.get(ACCOUNT_ACTIVITY).update(
|
||||
(activity) => {
|
||||
activity ||= {};
|
||||
activity[userId] = lastActivity;
|
||||
return activity;
|
||||
},
|
||||
{
|
||||
shouldUpdate: (oldActivity) => oldActivity?.[userId]?.getTime() !== lastActivity?.getTime(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async removeAccountActivity(userId: UserId): Promise<void> {
|
||||
await this.globalStateProvider.get(ACCOUNT_ACTIVITY).update(
|
||||
(activity) => {
|
||||
if (activity == null) {
|
||||
return activity;
|
||||
}
|
||||
delete activity[userId];
|
||||
return activity;
|
||||
},
|
||||
{ shouldUpdate: (oldActivity) => oldActivity?.[userId] != null },
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: update to use our own account status settings. Requires inverting direction of state service accounts flow
|
||||
async delete(): Promise<void> {
|
||||
try {
|
||||
|
|
|
@ -78,7 +78,7 @@ export class SystemService implements SystemServiceAbstraction {
|
|||
);
|
||||
if (timeoutAction === VaultTimeoutAction.LogOut) {
|
||||
const nextUser = await firstValueFrom(
|
||||
this.accountService.nextUpActiveUsers$.pipe(map((accounts) => accounts[0]?.id ?? null)),
|
||||
this.accountService.nextUpAccount$.pipe(map((account) => account?.id ?? null)),
|
||||
);
|
||||
// Can be removed once we migrate password generation history to state providers
|
||||
await this.stateService.clearDecryptedData(currentUser);
|
||||
|
|
|
@ -4,7 +4,6 @@ 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 { AccountActivityService } from "../../auth/abstractions/account-activity.service";
|
||||
import { AccountInfo } from "../../auth/abstractions/account.service";
|
||||
import { AuthService } from "../../auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
|
||||
|
@ -37,7 +36,6 @@ describe("VaultTimeoutService", () => {
|
|||
let authService: MockProxy<AuthService>;
|
||||
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
|
||||
let stateEventRunnerService: MockProxy<StateEventRunnerService>;
|
||||
let accountActivityService: MockProxy<AccountActivityService>;
|
||||
let lockedCallback: jest.Mock<Promise<void>, [userId: string]>;
|
||||
let loggedOutCallback: jest.Mock<Promise<void>, [expired: boolean, userId?: string]>;
|
||||
|
||||
|
@ -62,7 +60,6 @@ describe("VaultTimeoutService", () => {
|
|||
authService = mock();
|
||||
vaultTimeoutSettingsService = mock();
|
||||
stateEventRunnerService = mock();
|
||||
accountActivityService = mock();
|
||||
|
||||
lockedCallback = jest.fn();
|
||||
loggedOutCallback = jest.fn();
|
||||
|
@ -87,7 +84,6 @@ describe("VaultTimeoutService", () => {
|
|||
authService,
|
||||
vaultTimeoutSettingsService,
|
||||
stateEventRunnerService,
|
||||
accountActivityService,
|
||||
lockedCallback,
|
||||
loggedOutCallback,
|
||||
);
|
||||
|
@ -150,7 +146,7 @@ describe("VaultTimeoutService", () => {
|
|||
{} as Record<string, AccountInfo>,
|
||||
),
|
||||
);
|
||||
accountActivityService.accountActivity$ = of(
|
||||
accountService.accountActivity$ = of(
|
||||
Object.entries(accounts).reduce(
|
||||
(agg, [id, info]) => {
|
||||
agg[id] = info.lastActive ? new Date(info.lastActive) : null;
|
||||
|
|
|
@ -3,7 +3,6 @@ import { combineLatest, firstValueFrom, switchMap } from "rxjs";
|
|||
import { SearchService } from "../../abstractions/search.service";
|
||||
import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout.service";
|
||||
import { AccountActivityService } from "../../auth/abstractions/account-activity.service";
|
||||
import { AccountService } from "../../auth/abstractions/account.service";
|
||||
import { AuthService } from "../../auth/abstractions/auth.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../../auth/abstractions/master-password.service.abstraction";
|
||||
|
@ -37,7 +36,6 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
|
|||
private authService: AuthService,
|
||||
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||
private stateEventRunnerService: StateEventRunnerService,
|
||||
private accountActivityService: AccountActivityService,
|
||||
private lockedCallback: (userId?: string) => Promise<void> = null,
|
||||
private loggedOutCallback: (expired: boolean, userId?: string) => Promise<void> = null,
|
||||
) {}
|
||||
|
@ -69,7 +67,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
|
|||
await firstValueFrom(
|
||||
combineLatest([
|
||||
this.accountService.activeAccount$,
|
||||
this.accountActivityService.accountActivity$,
|
||||
this.accountService.accountActivity$,
|
||||
]).pipe(
|
||||
switchMap(async ([activeAccount, accountActivity]) => {
|
||||
const activeUserId = activeAccount?.id;
|
||||
|
|
Loading…
Reference in New Issue