1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-07 11:03:30 +00:00

[PM-5266] Create Avatar Service (#7905)

* rename file, move, and update imports

* refactor and implement StateProvider

* remove comments

* add migration

* use 'disk-local' for web

* add JSDoc comments

* move AvatarService before SyncService

* create factory

* replace old method with observable in story

* fix tests

* add tests for migration

* receive most recent avatarColor emission

* move logic to component

* fix CLI dependency

* remove BehaviorSubject

* cleanup

* use UserKeyDefinition

* avoid extra write

* convert to observable

* fix tests
This commit is contained in:
rr-bw
2024-03-14 09:56:48 -07:00
committed by GitHub
parent 10d503c15f
commit 65b7ca7177
25 changed files with 403 additions and 165 deletions

View File

@@ -0,0 +1,38 @@
import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service";
import { AvatarService } from "@bitwarden/common/auth/services/avatar.service";
import {
ApiServiceInitOptions,
apiServiceFactory,
} from "../../../platform/background/service-factories/api-service.factory";
import {
CachedServices,
factory,
FactoryOptions,
} from "../../../platform/background/service-factories/factory-options";
import {
stateProviderFactory,
StateProviderInitOptions,
} from "../../../platform/background/service-factories/state-provider.factory";
type AvatarServiceFactoryOptions = FactoryOptions;
export type AvatarServiceInitOptions = AvatarServiceFactoryOptions &
ApiServiceInitOptions &
StateProviderInitOptions;
export function avatarServiceFactory(
cache: { avatarService?: AvatarServiceAbstraction } & CachedServices,
opts: AvatarServiceInitOptions,
): Promise<AvatarServiceAbstraction> {
return factory(
cache,
"avatarService",
opts,
async () =>
new AvatarService(
await apiServiceFactory(cache, opts),
await stateProviderFactory(cache, opts),
),
);
}

View File

@@ -1,32 +1,61 @@
import { Location } from "@angular/common";
import { Component } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { Observable, combineLatest, switchMap } from "rxjs";
import { CurrentAccountService } from "./services/current-account.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { UserId } from "@bitwarden/common/types/guid";
export type CurrentAccount = {
id: UserId;
name: string | undefined;
email: string;
status: AuthenticationStatus;
avatarColor: string;
};
@Component({
selector: "app-current-account",
templateUrl: "current-account.component.html",
})
export class CurrentAccountComponent {
currentAccount$: Observable<CurrentAccount>;
constructor(
private currentAccountService: CurrentAccountService,
private accountService: AccountService,
private avatarService: AvatarService,
private router: Router,
private location: Location,
private route: ActivatedRoute,
) {}
) {
this.currentAccount$ = combineLatest([
this.accountService.activeAccount$,
this.avatarService.avatarColor$,
]).pipe(
switchMap(async ([account, avatarColor]) => {
if (account == null) {
return null;
}
const currentAccount: CurrentAccount = {
id: account.id,
name: account.name || account.email,
email: account.email,
status: account.status,
avatarColor,
};
get currentAccount$() {
return this.currentAccountService.currentAccount$;
return currentAccount;
}),
);
}
async currentAccountClicked() {
if (this.route.snapshot.data.state.includes("account-switcher")) {
this.location.back();
} else {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/account-switcher"]);
await this.router.navigate(["/account-switcher"]);
}
}
}

View File

@@ -1,12 +1,12 @@
import { matches, mock } from "jest-mock-extended";
import { BehaviorSubject, firstValueFrom, timeout } from "rxjs";
import { BehaviorSubject, firstValueFrom, of, timeout } from "rxjs";
import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { UserId } from "@bitwarden/common/types/guid";
import { AccountSwitcherService } from "./account-switcher.service";
@@ -16,7 +16,7 @@ describe("AccountSwitcherService", () => {
const activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>(null);
const accountService = mock<AccountService>();
const stateService = mock<StateService>();
const avatarService = mock<AvatarService>();
const messagingService = mock<MessagingService>();
const environmentService = mock<EnvironmentService>();
const logService = mock<LogService>();
@@ -25,11 +25,13 @@ describe("AccountSwitcherService", () => {
beforeEach(() => {
jest.resetAllMocks();
accountService.accounts$ = accountsSubject;
accountService.activeAccount$ = activeAccountSubject;
accountSwitcherService = new AccountSwitcherService(
accountService,
stateService,
avatarService,
messagingService,
environmentService,
logService,
@@ -44,6 +46,7 @@ describe("AccountSwitcherService", () => {
status: AuthenticationStatus.Unlocked,
};
avatarService.getUserAvatarColor$.mockReturnValue(of("#cccccc"));
accountsSubject.next({
"1": user1AccountInfo,
} as Record<UserId, AccountInfo>);
@@ -72,6 +75,7 @@ describe("AccountSwitcherService", () => {
status: AuthenticationStatus.Unlocked,
};
}
avatarService.getUserAvatarColor$.mockReturnValue(of("#cccccc"));
accountsSubject.next(seedAccounts);
activeAccountSubject.next(
Object.assign(seedAccounts["1" as UserId], { id: "1" as UserId }),

View File

@@ -11,11 +11,11 @@ import {
} from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { UserId } from "@bitwarden/common/types/guid";
import { fromChromeEvent } from "../../../../platform/browser/from-chrome-event";
@@ -44,7 +44,7 @@ export class AccountSwitcherService {
constructor(
private accountService: AccountService,
private stateService: StateService,
private avatarService: AvatarService,
private messagingService: MessagingService,
private environmentService: EnvironmentService,
private logService: LogService,
@@ -68,7 +68,9 @@ export class AccountSwitcherService {
server: await this.environmentService.getHost(id),
status: account.status,
isActive: id === activeAccount?.id,
avatarColor: await this.stateService.getAvatarColor({ userId: id }),
avatarColor: await firstValueFrom(
this.avatarService.getUserAvatarColor$(id as UserId),
),
};
}),
);

View File

@@ -1,44 +0,0 @@
import { Injectable } from "@angular/core";
import { Observable, switchMap } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { UserId } from "@bitwarden/common/types/guid";
export type CurrentAccount = {
id: UserId;
name: string | undefined;
email: string;
status: AuthenticationStatus;
avatarColor: string;
};
@Injectable({
providedIn: "root",
})
export class CurrentAccountService {
currentAccount$: Observable<CurrentAccount>;
constructor(
private accountService: AccountService,
private stateService: StateService,
) {
this.currentAccount$ = this.accountService.activeAccount$.pipe(
switchMap(async (account) => {
if (account == null) {
return null;
}
const currentAccount: CurrentAccount = {
id: account.id,
name: account.name || account.email,
email: account.email,
status: account.status,
avatarColor: await this.stateService.getAvatarColor({ userId: account.id }),
};
return currentAccount;
}),
);
}
}

View File

@@ -8,7 +8,6 @@ import {
AuthRequestServiceAbstraction,
AuthRequestService,
} from "@bitwarden/auth/common";
import { AvatarUpdateService as AvatarUpdateServiceAbstraction } from "@bitwarden/common/abstractions/account/avatar-update.service";
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service";
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
@@ -39,6 +38,7 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
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";
import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device-trust-crypto.service.implementation";
import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation";
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
@@ -117,7 +117,6 @@ import { DefaultStateProvider } from "@bitwarden/common/platform/state/implement
import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service";
/* eslint-enable import/no-restricted-paths */
import { DefaultThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { AvatarUpdateService } from "@bitwarden/common/services/account/avatar-update.service";
import { ApiService } from "@bitwarden/common/services/api.service";
import { AuditService } from "@bitwarden/common/services/audit.service";
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
@@ -125,6 +124,7 @@ import { EventUploadService } from "@bitwarden/common/services/event/event-uploa
import { NotificationsService } from "@bitwarden/common/services/notifications.service";
import { SearchService } from "@bitwarden/common/services/search.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/src/auth/abstractions/avatar.service";
import {
PasswordGenerationService,
PasswordGenerationServiceAbstraction,
@@ -288,7 +288,7 @@ export default class MainBackground {
fido2UserInterfaceService: Fido2UserInterfaceServiceAbstraction;
fido2AuthenticatorService: Fido2AuthenticatorServiceAbstraction;
fido2ClientService: Fido2ClientServiceAbstraction;
avatarUpdateService: AvatarUpdateServiceAbstraction;
avatarService: AvatarServiceAbstraction;
mainContextMenuHandler: MainContextMenuHandler;
cipherContextMenuHandler: CipherContextMenuHandler;
configService: BrowserConfigService;
@@ -685,7 +685,11 @@ export default class MainBackground {
this.fileUploadService,
this.sendService,
);
this.avatarService = new AvatarService(this.apiService, this.stateProvider);
this.providerService = new ProviderService(this.stateProvider);
this.syncService = new SyncService(
this.apiService,
this.domainSettingsService,
@@ -703,6 +707,7 @@ export default class MainBackground {
this.folderApiService,
this.organizationService,
this.sendApiService,
this.avatarService,
logoutCallback,
);
this.eventUploadService = new EventUploadService(
@@ -943,8 +948,6 @@ export default class MainBackground {
this.apiService,
);
this.avatarUpdateService = new AvatarUpdateService(this.apiService, this.stateService);
if (!this.popupOnlyContext) {
this.mainContextMenuHandler = new MainContextMenuHandler(
this.stateService,

View File

@@ -130,9 +130,6 @@ export default class RuntimeBackground {
await this.main.refreshBadge();
await this.main.refreshMenu();
}, 2000);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.main.avatarUpdateService.loadColorFromState();
this.configService.triggerServerConfigFetch();
}
break;