-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
account-switcher.service.ts
164 lines (146 loc) · 5.7 KB
/
account-switcher.service.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
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 { AuthService } from "@bitwarden/common/auth/abstractions/auth.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 { 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 avatarService: AvatarService,
private messagingService: MessagingService,
private environmentService: EnvironmentService,
private logService: LogService,
authService: AuthService,
) {
this.availableAccounts$ = combineLatest([
accountService.accounts$,
authService.authStatuses$,
this.accountService.activeAccount$,
]).pipe(
switchMap(async ([accounts, accountStatuses, activeAccount]) => {
const loggedInIds = Object.keys(accounts).filter(
(id: UserId) => accountStatuses[id] !== AuthenticationStatus.LoggedOut,
);
// Accounts shouldn't ever be more than ACCOUNT_LIMIT but just in case do a greater than
const hasMaxAccounts = loggedInIds.length >= this.ACCOUNT_LIMIT;
const options: AvailableAccount[] = await Promise.all(
loggedInIds.map(async (id: UserId) => {
return {
name: accounts[id].name ?? accounts[id].email,
email: accounts[id].email,
id: id,
server: (await this.environmentService.getEnvironment(id))?.getHostname(),
status: accountStatuses[id],
isActive: id === activeAccount?.id,
avatarColor: await firstValueFrom(
this.avatarService.getUserAvatarColor$(id as UserId),
),
};
}),
);
if (!hasMaxAccounts) {
options.push({
name: "Add account",
id: this.SPECIAL_ADD_ACCOUNT_ID,
isActive: false,
});
}
return options.sort((a, b) => {
/**
* Make sure the compare function is "well-formed" to account for browser inconsistencies.
*
* For specifics, see the sections "Description" and "Sorting with a non-well-formed comparator"
* on this page:
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
*/
// Active account (if one exists) is always first
if (a.isActive) {
return -1;
}
// If account "b" is the 'Add account' button, keep original order of "a" and "b"
if (b.id === this.SPECIAL_ADD_ACCOUNT_ID) {
return 0;
}
return 1;
});
}),
);
// Create a reusable observable that listens to 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;
});
}
}