mirror of
https://github.com/bitwarden/browser.git
synced 2024-09-28 04:08:47 +02:00
140 lines
4.8 KiB
TypeScript
140 lines
4.8 KiB
TypeScript
|
import { Injectable } from "@angular/core";
|
||
|
import {
|
||
|
Observable,
|
||
|
combineLatest,
|
||
|
filter,
|
||
|
firstValueFrom,
|
||
|
map,
|
||
|
switchMap,
|
||
|
throwError,
|
||
|
timeout,
|
||
|
} from "rxjs";
|
||
|
|
||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.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";
|
||
|
|
||
|
export type AvailableAccount = {
|
||
|
name: string;
|
||
|
email?: string;
|
||
|
id: string;
|
||
|
isActive: boolean;
|
||
|
server?: string;
|
||
|
status?: AuthenticationStatus;
|
||
|
avatarColor?: string;
|
||
|
};
|
||
|
|
||
|
@Injectable({
|
||
|
providedIn: "root",
|
||
|
})
|
||
|
export class AccountSwitcherService {
|
||
|
static incompleteAccountSwitchError = "Account switch did not complete.";
|
||
|
|
||
|
ACCOUNT_LIMIT = 5;
|
||
|
SPECIAL_ADD_ACCOUNT_ID = "addAccount";
|
||
|
availableAccounts$: Observable<AvailableAccount[]>;
|
||
|
|
||
|
switchAccountFinished$: Observable<string>;
|
||
|
|
||
|
constructor(
|
||
|
private accountService: AccountService,
|
||
|
private stateService: StateService,
|
||
|
private messagingService: MessagingService,
|
||
|
private environmentService: EnvironmentService,
|
||
|
private logService: LogService,
|
||
|
) {
|
||
|
this.availableAccounts$ = combineLatest([
|
||
|
this.accountService.accounts$,
|
||
|
this.accountService.activeAccount$,
|
||
|
]).pipe(
|
||
|
switchMap(async ([accounts, activeAccount]) => {
|
||
|
const accountEntries = Object.entries(accounts).filter(
|
||
|
([_, account]) => account.status !== AuthenticationStatus.LoggedOut,
|
||
|
);
|
||
|
// 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 options: AvailableAccount[] = await Promise.all(
|
||
|
accountEntries.map(async ([id, account]) => {
|
||
|
return {
|
||
|
name: account.name ?? account.email,
|
||
|
email: account.email,
|
||
|
id: id,
|
||
|
server: await this.environmentService.getHost(id),
|
||
|
status: account.status,
|
||
|
isActive: id === activeAccount?.id,
|
||
|
avatarColor: await this.stateService.getAvatarColor({ userId: id }),
|
||
|
};
|
||
|
}),
|
||
|
);
|
||
|
|
||
|
if (!hasMaxAccounts) {
|
||
|
options.push({
|
||
|
name: "Add account",
|
||
|
id: this.SPECIAL_ADD_ACCOUNT_ID,
|
||
|
isActive: activeAccount?.id == null,
|
||
|
});
|
||
|
}
|
||
|
|
||
|
return options;
|
||
|
}),
|
||
|
);
|
||
|
|
||
|
// Create a reusable observable that listens to the the switchAccountFinish message and returns the userId from the message
|
||
|
this.switchAccountFinished$ = fromChromeEvent<[message: { command: string; userId: string }]>(
|
||
|
chrome.runtime.onMessage,
|
||
|
).pipe(
|
||
|
filter(([message]) => message.command === "switchAccountFinish"),
|
||
|
map(([message]) => message.userId),
|
||
|
);
|
||
|
}
|
||
|
|
||
|
get specialAccountAddId() {
|
||
|
return this.SPECIAL_ADD_ACCOUNT_ID;
|
||
|
}
|
||
|
|
||
|
async selectAccount(id: string) {
|
||
|
if (id === this.SPECIAL_ADD_ACCOUNT_ID) {
|
||
|
id = null;
|
||
|
}
|
||
|
|
||
|
// Creates a subscription to the switchAccountFinished observable but further
|
||
|
// filters it to only care about the current userId.
|
||
|
const switchAccountFinishedPromise = firstValueFrom(
|
||
|
this.switchAccountFinished$.pipe(
|
||
|
filter((userId) => userId === id),
|
||
|
timeout({
|
||
|
// Much longer than account switching is expected to take for normal accounts
|
||
|
// but the account switching process includes a possible full sync so we need to account
|
||
|
// for very large accounts and want to still have a timeout
|
||
|
// to avoid a promise that might never resolve/reject
|
||
|
first: 60_000,
|
||
|
with: () =>
|
||
|
throwError(() => new Error(AccountSwitcherService.incompleteAccountSwitchError)),
|
||
|
}),
|
||
|
),
|
||
|
);
|
||
|
|
||
|
// Initiate the actions required to make account switching happen
|
||
|
await this.accountService.switchAccount(id as UserId);
|
||
|
this.messagingService.send("switchAccount", { userId: id }); // This message should cause switchAccountFinish to be sent
|
||
|
|
||
|
// Wait until we recieve the switchAccountFinished message
|
||
|
await switchAccountFinishedPromise.catch((err) => {
|
||
|
if (
|
||
|
err instanceof Error &&
|
||
|
err.message === AccountSwitcherService.incompleteAccountSwitchError
|
||
|
) {
|
||
|
this.logService.warning("message 'switchAccount' never responded.");
|
||
|
return;
|
||
|
}
|
||
|
throw err;
|
||
|
});
|
||
|
}
|
||
|
}
|