mirror of
https://github.com/bitwarden/browser
synced 2025-12-17 08:43: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:
@@ -49,7 +49,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="account-switcher-row tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-p-3 disabled:tw-cursor-not-allowed disabled:tw-border-text-muted/60 disabled:!tw-text-muted/60"
|
||||
(click)="lock()"
|
||||
(click)="lock(currentAccount.id)"
|
||||
[disabled]="currentAccount.status === lockedStatus || !activeUserCanLock"
|
||||
[title]="!activeUserCanLock ? ('unlockMethodNeeded' | i18n) : ''"
|
||||
>
|
||||
@@ -59,7 +59,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="account-switcher-row tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-p-3"
|
||||
(click)="logOut()"
|
||||
(click)="logOut(currentAccount.id)"
|
||||
>
|
||||
<i class="bwi bwi-sign-out tw-text-2xl" aria-hidden="true"></i>
|
||||
{{ "logOut" | i18n }}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { AccountSwitcherService } from "./services/account-switcher.service";
|
||||
@@ -64,9 +65,9 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
||||
this.location.back();
|
||||
}
|
||||
|
||||
async lock(userId?: string) {
|
||||
async lock(userId: string) {
|
||||
this.loading = true;
|
||||
await this.vaultTimeoutService.lock(userId ? userId : null);
|
||||
await this.vaultTimeoutService.lock(userId);
|
||||
// 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(["lock"]);
|
||||
@@ -96,7 +97,7 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
||||
.subscribe(() => this.router.navigate(["lock"]));
|
||||
}
|
||||
|
||||
async logOut() {
|
||||
async logOut(userId: UserId) {
|
||||
this.loading = true;
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "logOut" },
|
||||
@@ -105,7 +106,7 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
this.messagingService.send("logout");
|
||||
this.messagingService.send("logout", { userId });
|
||||
}
|
||||
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
|
||||
@@ -58,6 +58,7 @@ describe("AccountSwitcherService", () => {
|
||||
const accountInfo: AccountInfo = {
|
||||
name: "Test User 1",
|
||||
email: "test1@email.com",
|
||||
emailVerified: true,
|
||||
};
|
||||
|
||||
avatarService.getUserAvatarColor$.mockReturnValue(of("#cccccc"));
|
||||
@@ -89,6 +90,7 @@ describe("AccountSwitcherService", () => {
|
||||
for (let i = 0; i < numberOfAccounts; i++) {
|
||||
seedAccounts[`${i}` as UserId] = {
|
||||
email: `test${i}@email.com`,
|
||||
emailVerified: true,
|
||||
name: "Test User ${i}",
|
||||
};
|
||||
seedStatuses[`${i}` as UserId] = AuthenticationStatus.Unlocked;
|
||||
@@ -113,6 +115,7 @@ describe("AccountSwitcherService", () => {
|
||||
const user1AccountInfo: AccountInfo = {
|
||||
name: "Test User 1",
|
||||
email: "",
|
||||
emailVerified: true,
|
||||
};
|
||||
accountsSubject.next({ ["1" as UserId]: user1AccountInfo });
|
||||
authStatusSubject.next({ ["1" as UserId]: AuthenticationStatus.LoggedOut });
|
||||
|
||||
@@ -59,7 +59,7 @@ export class LockComponent extends BaseLockComponent {
|
||||
policyApiService: PolicyApiServiceAbstraction,
|
||||
policyService: InternalPolicyService,
|
||||
passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||
private authService: AuthService,
|
||||
authService: AuthService,
|
||||
dialogService: DialogService,
|
||||
deviceTrustService: DeviceTrustServiceAbstraction,
|
||||
userVerificationService: UserVerificationService,
|
||||
@@ -92,6 +92,7 @@ export class LockComponent extends BaseLockComponent {
|
||||
pinCryptoService,
|
||||
biometricStateService,
|
||||
accountService,
|
||||
authService,
|
||||
kdfConfigService,
|
||||
);
|
||||
this.successRoute = "/tabs/current";
|
||||
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
GENERATE_PASSWORD_ID,
|
||||
NOOP_COMMAND_SUFFIX,
|
||||
} from "@bitwarden/common/autofill/constants";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
@@ -65,7 +66,7 @@ describe("ContextMenuClickedHandler", () => {
|
||||
let autofill: AutofillAction;
|
||||
let authService: MockProxy<AuthService>;
|
||||
let cipherService: MockProxy<CipherService>;
|
||||
let stateService: MockProxy<StateService>;
|
||||
let accountService: FakeAccountService;
|
||||
let totpService: MockProxy<TotpService>;
|
||||
let eventCollectionService: MockProxy<EventCollectionService>;
|
||||
let userVerificationService: MockProxy<UserVerificationService>;
|
||||
@@ -78,7 +79,7 @@ describe("ContextMenuClickedHandler", () => {
|
||||
autofill = jest.fn<Promise<void>, [tab: chrome.tabs.Tab, cipher: CipherView]>();
|
||||
authService = mock();
|
||||
cipherService = mock();
|
||||
stateService = mock();
|
||||
accountService = mockAccountServiceWith("userId" as UserId);
|
||||
totpService = mock();
|
||||
eventCollectionService = mock();
|
||||
|
||||
@@ -88,10 +89,10 @@ describe("ContextMenuClickedHandler", () => {
|
||||
autofill,
|
||||
authService,
|
||||
cipherService,
|
||||
stateService,
|
||||
totpService,
|
||||
eventCollectionService,
|
||||
userVerificationService,
|
||||
accountService,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.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";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
@@ -17,7 +20,6 @@ import {
|
||||
NOOP_COMMAND_SUFFIX,
|
||||
} from "@bitwarden/common/autofill/constants";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
@@ -26,6 +28,7 @@ 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 { accountServiceFactory } from "../../auth/background/service-factories/account-service.factory";
|
||||
import {
|
||||
authServiceFactory,
|
||||
AuthServiceInitOptions,
|
||||
@@ -37,7 +40,6 @@ import { autofillSettingsServiceFactory } from "../../autofill/background/servic
|
||||
import { eventCollectionServiceFactory } from "../../background/service-factories/event-collection-service.factory";
|
||||
import { Account } from "../../models/account";
|
||||
import { CachedServices } from "../../platform/background/service-factories/factory-options";
|
||||
import { stateServiceFactory } from "../../platform/background/service-factories/state-service.factory";
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
import { passwordGenerationServiceFactory } from "../../tools/background/service_factories/password-generation-service.factory";
|
||||
import {
|
||||
@@ -71,10 +73,10 @@ export class ContextMenuClickedHandler {
|
||||
private autofillAction: AutofillAction,
|
||||
private authService: AuthService,
|
||||
private cipherService: CipherService,
|
||||
private stateService: StateService,
|
||||
private totpService: TotpService,
|
||||
private eventCollectionService: EventCollectionService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
static async mv3Create(cachedServices: CachedServices) {
|
||||
@@ -128,10 +130,10 @@ export class ContextMenuClickedHandler {
|
||||
(tab, cipher) => autofillCommand.doAutofillTabWithCipherCommand(tab, cipher),
|
||||
await authServiceFactory(cachedServices, serviceOptions),
|
||||
await cipherServiceFactory(cachedServices, serviceOptions),
|
||||
await stateServiceFactory(cachedServices, serviceOptions),
|
||||
await totpServiceFactory(cachedServices, serviceOptions),
|
||||
await eventCollectionServiceFactory(cachedServices, serviceOptions),
|
||||
await userVerificationServiceFactory(cachedServices, serviceOptions),
|
||||
await accountServiceFactory(cachedServices, serviceOptions),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -239,9 +241,10 @@ export class ContextMenuClickedHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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.stateService.setLastActive(new Date().getTime());
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
await this.accountService.setAccountActivity(activeUserId, new Date());
|
||||
switch (info.parentMenuItemId) {
|
||||
case AUTOFILL_ID:
|
||||
case AUTOFILL_IDENTITY_ID:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Subject, firstValueFrom, merge, timeout } from "rxjs";
|
||||
import { Subject, firstValueFrom, map, merge, timeout } from "rxjs";
|
||||
|
||||
import {
|
||||
PinCryptoServiceAbstraction,
|
||||
@@ -902,6 +902,7 @@ export default class MainBackground {
|
||||
this.autofillSettingsService,
|
||||
this.vaultTimeoutSettingsService,
|
||||
this.biometricStateService,
|
||||
this.accountService,
|
||||
);
|
||||
|
||||
// Other fields
|
||||
@@ -920,7 +921,6 @@ export default class MainBackground {
|
||||
this.autofillService,
|
||||
this.platformUtilsService as BrowserPlatformUtilsService,
|
||||
this.notificationsService,
|
||||
this.stateService,
|
||||
this.autofillSettingsService,
|
||||
this.systemService,
|
||||
this.environmentService,
|
||||
@@ -929,6 +929,7 @@ export default class MainBackground {
|
||||
this.configService,
|
||||
this.fido2Background,
|
||||
messageListener,
|
||||
this.accountService,
|
||||
);
|
||||
this.nativeMessagingBackground = new NativeMessagingBackground(
|
||||
this.accountService,
|
||||
@@ -1018,10 +1019,10 @@ export default class MainBackground {
|
||||
},
|
||||
this.authService,
|
||||
this.cipherService,
|
||||
this.stateService,
|
||||
this.totpService,
|
||||
this.eventCollectionService,
|
||||
this.userVerificationService,
|
||||
this.accountService,
|
||||
);
|
||||
|
||||
this.contextMenusBackground = new ContextMenusBackground(contextMenuClickedHandler);
|
||||
@@ -1168,7 +1169,12 @@ export default class MainBackground {
|
||||
*/
|
||||
async switchAccount(userId: UserId) {
|
||||
try {
|
||||
await this.stateService.setActiveUser(userId);
|
||||
const currentlyActiveAccount = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((account) => account?.id)),
|
||||
);
|
||||
// can be removed once password generation history is migrated to state providers
|
||||
await this.stateService.clearDecryptedData(currentlyActiveAccount);
|
||||
await this.accountService.switchAccount(userId);
|
||||
|
||||
if (userId == null) {
|
||||
this.loginEmailService.setRememberEmail(false);
|
||||
@@ -1240,7 +1246,11 @@ export default class MainBackground {
|
||||
//Needs to be checked before state is cleaned
|
||||
const needStorageReseed = await this.needsStorageReseed();
|
||||
|
||||
const newActiveUser = await this.stateService.clean({ userId: userId });
|
||||
const newActiveUser = await firstValueFrom(
|
||||
this.accountService.nextUpAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
await this.stateService.clean({ userId: userId });
|
||||
await this.accountService.clean(userId);
|
||||
|
||||
await this.stateEventRunnerService.handleEvent("logout", userId);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { firstValueFrom, mergeMap } from "rxjs";
|
||||
import { firstValueFrom, map, mergeMap } from "rxjs";
|
||||
|
||||
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.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";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
@@ -19,7 +20,6 @@ import {
|
||||
import { LockedVaultPendingNotificationsData } from "../autofill/background/abstractions/notification.background";
|
||||
import { AutofillService } from "../autofill/services/abstractions/autofill.service";
|
||||
import { BrowserApi } from "../platform/browser/browser-api";
|
||||
import { BrowserStateService } from "../platform/services/abstractions/browser-state.service";
|
||||
import { BrowserEnvironmentService } from "../platform/services/browser-environment.service";
|
||||
import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service";
|
||||
import { Fido2Background } from "../vault/fido2/background/abstractions/fido2.background";
|
||||
@@ -37,7 +37,6 @@ export default class RuntimeBackground {
|
||||
private autofillService: AutofillService,
|
||||
private platformUtilsService: BrowserPlatformUtilsService,
|
||||
private notificationsService: NotificationsService,
|
||||
private stateService: BrowserStateService,
|
||||
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
||||
private systemService: SystemService,
|
||||
private environmentService: BrowserEnvironmentService,
|
||||
@@ -46,6 +45,7 @@ export default class RuntimeBackground {
|
||||
private configService: ConfigService,
|
||||
private fido2Background: Fido2Background,
|
||||
private messageListener: MessageListener,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
// onInstalled listener must be wired up before anything else, so we do it in the ctor
|
||||
chrome.runtime.onInstalled.addListener((details: any) => {
|
||||
@@ -111,9 +111,10 @@ export default class RuntimeBackground {
|
||||
switch (msg.sender) {
|
||||
case "autofiller":
|
||||
case "autofill_cmd": {
|
||||
// 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.stateService.setLastActive(new Date().getTime());
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
await this.accountService.setAccountActivity(activeUserId, new Date());
|
||||
const totpCode = await this.autofillService.doAutoFillActiveTab(
|
||||
[
|
||||
{
|
||||
|
||||
@@ -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));
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
@@ -50,7 +49,6 @@ describe("Browser State Service", () => {
|
||||
state.accounts[userId] = new Account({
|
||||
profile: { userId: userId },
|
||||
});
|
||||
state.activeUserId = userId;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -78,18 +76,8 @@ describe("Browser State Service", () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe("add Account", () => {
|
||||
it("should add account", async () => {
|
||||
const newUserId = "newUserId" as UserId;
|
||||
const newAcct = new Account({
|
||||
profile: { userId: newUserId },
|
||||
});
|
||||
|
||||
await sut.addAccount(newAcct);
|
||||
|
||||
const accts = await firstValueFrom(sut.accounts$);
|
||||
expect(accts[newUserId]).toBeDefined();
|
||||
});
|
||||
it("exists", () => {
|
||||
expect(sut).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,8 +29,6 @@ export class DefaultBrowserStateService
|
||||
initializeAs: "record",
|
||||
})
|
||||
protected accountsSubject: BehaviorSubject<{ [userId: string]: Account }>;
|
||||
@sessionSync({ initializer: (s: string) => s })
|
||||
protected activeAccountSubject: BehaviorSubject<string>;
|
||||
|
||||
protected accountDeserializer = Account.fromJSON;
|
||||
|
||||
|
||||
@@ -200,26 +200,29 @@ 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;
|
||||
}
|
||||
|
||||
return JSON.stringify(value1) === JSON.stringify(value2);
|
||||
} 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||
import { NavigationEnd, Router, RouterOutlet } from "@angular/router";
|
||||
import { filter, concatMap, Subject, takeUntil, firstValueFrom, tap, map } from "rxjs";
|
||||
import { Subject, takeUntil, firstValueFrom, concatMap, filter, tap } 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { MessageListener } from "@bitwarden/common/platform/messaging";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components";
|
||||
|
||||
@@ -27,8 +29,9 @@ import { DesktopSyncVerificationDialogComponent } from "./components/desktop-syn
|
||||
</div>`,
|
||||
})
|
||||
export class AppComponent implements OnInit, OnDestroy {
|
||||
private lastActivity: number = null;
|
||||
private activeUserId: string;
|
||||
private lastActivity: Date;
|
||||
private activeUserId: UserId;
|
||||
private recordActivitySubject = new Subject<void>();
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
@@ -46,6 +49,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
private dialogService: DialogService,
|
||||
private messageListener: MessageListener,
|
||||
private toastService: ToastService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -53,14 +57,13 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
// Clear them aggressively to make sure this doesn't occur
|
||||
await this.clearComponentStates();
|
||||
|
||||
this.stateService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((userId) => {
|
||||
this.activeUserId = userId;
|
||||
this.accountService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((account) => {
|
||||
this.activeUserId = account?.id;
|
||||
});
|
||||
|
||||
this.authService.activeAccountStatus$
|
||||
.pipe(
|
||||
map((status) => status === AuthenticationStatus.Unlocked),
|
||||
filter((unlocked) => unlocked),
|
||||
filter((status) => status === AuthenticationStatus.Unlocked),
|
||||
concatMap(async () => {
|
||||
await this.recordActivity();
|
||||
}),
|
||||
@@ -200,13 +203,13 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = new Date().getTime();
|
||||
if (this.lastActivity != null && now - this.lastActivity < 250) {
|
||||
const now = new Date();
|
||||
if (this.lastActivity != null && now.getTime() - this.lastActivity.getTime() < 250) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastActivity = now;
|
||||
await this.stateService.setLastActive(now, { userId: this.activeUserId });
|
||||
await this.accountService.setAccountActivity(this.activeUserId, now);
|
||||
}
|
||||
|
||||
private showToast(msg: any) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { first } from "rxjs/operators";
|
||||
|
||||
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/tools/send/add-edit.component";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -51,6 +52,7 @@ export class SendAddEditComponent extends BaseAddEditComponent {
|
||||
formBuilder: FormBuilder,
|
||||
private filePopoutUtilsService: FilePopoutUtilsService,
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
accountService: AccountService,
|
||||
) {
|
||||
super(
|
||||
i18nService,
|
||||
@@ -66,6 +68,7 @@ export class SendAddEditComponent extends BaseAddEditComponent {
|
||||
dialogService,
|
||||
formBuilder,
|
||||
billingAccountProfileStateService,
|
||||
accountService,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -731,7 +731,7 @@ export class Main {
|
||||
this.authService.logOut(() => {
|
||||
/* Do nothing */
|
||||
});
|
||||
const userId = await this.stateService.getUserId();
|
||||
const userId = (await this.stateService.getUserId()) as UserId;
|
||||
await Promise.all([
|
||||
this.eventUploadService.uploadEvents(userId as UserId),
|
||||
this.syncService.setLastSync(new Date(0)),
|
||||
@@ -742,9 +742,10 @@ export class Main {
|
||||
this.passwordGenerationService.clear(),
|
||||
]);
|
||||
|
||||
await this.stateEventRunnerService.handleEvent("logout", userId as UserId);
|
||||
await this.stateEventRunnerService.handleEvent("logout", userId);
|
||||
|
||||
await this.stateService.clean();
|
||||
await this.accountService.clean(userId);
|
||||
process.env.BW_SESSION = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from "@bitwarden/angular/auth/guards";
|
||||
|
||||
import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component";
|
||||
import { LoginGuard } from "../auth/guards/login.guard";
|
||||
import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard";
|
||||
import { HintComponent } from "../auth/hint.component";
|
||||
import { LockComponent } from "../auth/lock.component";
|
||||
import { LoginDecryptionOptionsComponent } from "../auth/login/login-decryption-options/login-decryption-options.component";
|
||||
@@ -40,7 +40,7 @@ const routes: Routes = [
|
||||
{
|
||||
path: "login",
|
||||
component: LoginComponent,
|
||||
canActivate: [LoginGuard],
|
||||
canActivate: [maxAccountsGuardFn()],
|
||||
},
|
||||
{
|
||||
path: "login-with-device",
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
ViewContainerRef,
|
||||
} from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom, Subject, takeUntil } from "rxjs";
|
||||
import { firstValueFrom, map, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
@@ -18,9 +18,9 @@ import { NotificationsService } from "@bitwarden/common/abstractions/notificatio
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.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 { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.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";
|
||||
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
@@ -107,11 +107,11 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
|
||||
loading = false;
|
||||
|
||||
private lastActivity: number = null;
|
||||
private lastActivity: Date = null;
|
||||
private modal: ModalRef = null;
|
||||
private idleTimer: number = null;
|
||||
private isIdle = false;
|
||||
private activeUserId: string = null;
|
||||
private activeUserId: UserId = null;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
@@ -150,12 +150,12 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
private biometricStateService: BiometricStateService,
|
||||
private stateEventRunnerService: StateEventRunnerService,
|
||||
private providerService: ProviderService,
|
||||
private organizationService: InternalOrganizationServiceAbstraction,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.stateService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((userId) => {
|
||||
this.activeUserId = userId;
|
||||
this.accountService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((account) => {
|
||||
this.activeUserId = account?.id;
|
||||
});
|
||||
|
||||
this.ngZone.runOutsideAngular(() => {
|
||||
@@ -400,7 +400,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
break;
|
||||
case "switchAccount": {
|
||||
if (message.userId != null) {
|
||||
await this.stateService.setActiveUser(message.userId);
|
||||
await this.stateService.clearDecryptedData(message.userId);
|
||||
await this.accountService.switchAccount(message.userId);
|
||||
}
|
||||
const locked =
|
||||
(await this.authService.getAuthStatus(message.userId)) ===
|
||||
@@ -522,7 +523,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
|
||||
private async updateAppMenu() {
|
||||
let updateRequest: MenuUpdateRequest;
|
||||
const stateAccounts = await firstValueFrom(this.stateService.accounts$);
|
||||
const stateAccounts = await firstValueFrom(this.accountService.accounts$);
|
||||
if (stateAccounts == null || Object.keys(stateAccounts).length < 1) {
|
||||
updateRequest = {
|
||||
accounts: null,
|
||||
@@ -531,32 +532,32 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
} else {
|
||||
const accounts: { [userId: string]: MenuAccount } = {};
|
||||
for (const i in stateAccounts) {
|
||||
const userId = i as UserId;
|
||||
if (
|
||||
i != null &&
|
||||
stateAccounts[i]?.profile?.userId != null &&
|
||||
!this.isAccountCleanUpInProgress(stateAccounts[i].profile.userId) // skip accounts that are being cleaned up
|
||||
userId != null &&
|
||||
!this.isAccountCleanUpInProgress(userId) // skip accounts that are being cleaned up
|
||||
) {
|
||||
const userId = stateAccounts[i].profile.userId;
|
||||
const availableTimeoutActions = await firstValueFrom(
|
||||
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(userId),
|
||||
);
|
||||
|
||||
const authStatus = await firstValueFrom(this.authService.authStatusFor$(userId));
|
||||
accounts[userId] = {
|
||||
isAuthenticated: await this.stateService.getIsAuthenticated({
|
||||
userId: userId,
|
||||
}),
|
||||
isLocked:
|
||||
(await this.authService.getAuthStatus(userId)) === AuthenticationStatus.Locked,
|
||||
isAuthenticated: authStatus >= AuthenticationStatus.Locked,
|
||||
isLocked: authStatus === AuthenticationStatus.Locked,
|
||||
isLockable: availableTimeoutActions.includes(VaultTimeoutAction.Lock),
|
||||
email: stateAccounts[i].profile.email,
|
||||
userId: stateAccounts[i].profile.userId,
|
||||
email: stateAccounts[userId].email,
|
||||
userId: userId,
|
||||
hasMasterPassword: await this.userVerificationService.hasMasterPassword(userId),
|
||||
};
|
||||
}
|
||||
}
|
||||
updateRequest = {
|
||||
accounts: accounts,
|
||||
activeUserId: await this.stateService.getUserId(),
|
||||
activeUserId: await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -564,7 +565,9 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private async logOut(expired: boolean, userId?: string) {
|
||||
const userBeingLoggedOut = await this.stateService.getUserId({ userId: userId });
|
||||
const userBeingLoggedOut =
|
||||
(userId as UserId) ??
|
||||
(await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))));
|
||||
|
||||
// Mark account as being cleaned up so that the updateAppMenu logic (executed on syncCompleted)
|
||||
// doesn't attempt to update a user that is being logged out as we will manually
|
||||
@@ -572,9 +575,10 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
this.startAccountCleanUp(userBeingLoggedOut);
|
||||
|
||||
let preLogoutActiveUserId;
|
||||
const nextUpAccount = await firstValueFrom(this.accountService.nextUpAccount$);
|
||||
try {
|
||||
// Provide the userId of the user to upload events for
|
||||
await this.eventUploadService.uploadEvents(userBeingLoggedOut as UserId);
|
||||
await this.eventUploadService.uploadEvents(userBeingLoggedOut);
|
||||
await this.syncService.setLastSync(new Date(0), userBeingLoggedOut);
|
||||
await this.cryptoService.clearKeys(userBeingLoggedOut);
|
||||
await this.cipherService.clear(userBeingLoggedOut);
|
||||
@@ -582,22 +586,23 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
await this.collectionService.clear(userBeingLoggedOut);
|
||||
await this.passwordGenerationService.clear(userBeingLoggedOut);
|
||||
await this.vaultTimeoutSettingsService.clear(userBeingLoggedOut);
|
||||
await this.biometricStateService.logout(userBeingLoggedOut as UserId);
|
||||
await this.biometricStateService.logout(userBeingLoggedOut);
|
||||
|
||||
await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut as UserId);
|
||||
await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut);
|
||||
|
||||
preLogoutActiveUserId = this.activeUserId;
|
||||
await this.stateService.clean({ userId: userBeingLoggedOut });
|
||||
await this.accountService.clean(userBeingLoggedOut);
|
||||
} finally {
|
||||
this.finishAccountCleanUp(userBeingLoggedOut);
|
||||
}
|
||||
|
||||
if (this.activeUserId == null) {
|
||||
if (nextUpAccount == null) {
|
||||
// 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(["login"]);
|
||||
} else if (preLogoutActiveUserId !== this.activeUserId) {
|
||||
this.messagingService.send("switchAccount");
|
||||
} else if (preLogoutActiveUserId !== nextUpAccount.id) {
|
||||
this.messagingService.send("switchAccount", { userId: nextUpAccount.id });
|
||||
}
|
||||
|
||||
await this.updateAppMenu();
|
||||
@@ -622,13 +627,13 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = new Date().getTime();
|
||||
if (this.lastActivity != null && now - this.lastActivity < 250) {
|
||||
const now = new Date();
|
||||
if (this.lastActivity != null && now.getTime() - this.lastActivity.getTime() < 250) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastActivity = now;
|
||||
await this.stateService.setLastActive(now, { userId: this.activeUserId });
|
||||
await this.accountService.setAccountActivity(this.activeUserId, now);
|
||||
|
||||
// Idle states
|
||||
if (this.isIdle) {
|
||||
|
||||
@@ -1,110 +1,112 @@
|
||||
<!-- Please remove this disable statement when editing this file! -->
|
||||
<!-- eslint-disable @angular-eslint/template/button-has-type -->
|
||||
<button
|
||||
class="account-switcher"
|
||||
(click)="toggle()"
|
||||
cdkOverlayOrigin
|
||||
#trigger="cdkOverlayOrigin"
|
||||
[hidden]="!showSwitcher"
|
||||
aria-haspopup="dialog"
|
||||
>
|
||||
<ng-container *ngIf="activeAccount?.email != null; else noActiveAccount">
|
||||
<app-avatar
|
||||
[text]="activeAccount.name"
|
||||
[id]="activeAccount.id"
|
||||
[color]="activeAccount.avatarColor"
|
||||
[size]="25"
|
||||
[circle]="true"
|
||||
[fontSize]="14"
|
||||
[dynamic]="true"
|
||||
*ngIf="activeAccount.email != null"
|
||||
aria-hidden="true"
|
||||
></app-avatar>
|
||||
<div class="active-account">
|
||||
<div>{{ activeAccount.email }}</div>
|
||||
<span>{{ activeAccount.server }}</span>
|
||||
<span class="sr-only"> ({{ "switchAccount" | i18n }})</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #noActiveAccount>
|
||||
<span>{{ "switchAccount" | i18n }}</span>
|
||||
</ng-template>
|
||||
<i
|
||||
class="bwi"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-angle-down': !isOpen, 'bwi-angle-up': isOpen }"
|
||||
></i>
|
||||
</button>
|
||||
|
||||
<ng-template
|
||||
cdkConnectedOverlay
|
||||
[cdkConnectedOverlayOrigin]="trigger"
|
||||
[cdkConnectedOverlayHasBackdrop]="true"
|
||||
[cdkConnectedOverlayBackdropClass]="'cdk-overlay-transparent-backdrop'"
|
||||
(backdropClick)="close()"
|
||||
(detach)="close()"
|
||||
[cdkConnectedOverlayOpen]="showSwitcher && isOpen"
|
||||
[cdkConnectedOverlayPositions]="overlayPosition"
|
||||
cdkConnectedOverlayMinWidth="250px"
|
||||
>
|
||||
<div
|
||||
class="account-switcher-dropdown"
|
||||
[@transformPanel]="'open'"
|
||||
cdkTrapFocus
|
||||
cdkTrapFocusAutoCapture
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
<ng-container *ngIf="view$ | async as view">
|
||||
<button
|
||||
class="account-switcher"
|
||||
(click)="toggle()"
|
||||
cdkOverlayOrigin
|
||||
#trigger="cdkOverlayOrigin"
|
||||
[hidden]="!view.showSwitcher"
|
||||
aria-haspopup="dialog"
|
||||
>
|
||||
<div class="accounts" *ngIf="numberOfAccounts > 0">
|
||||
<button
|
||||
*ngFor="let account of inactiveAccounts | keyvalue"
|
||||
class="account"
|
||||
(click)="switch(account.key)"
|
||||
>
|
||||
<app-avatar
|
||||
[text]="account.value.name ?? account.value.email"
|
||||
[id]="account.value.id"
|
||||
[size]="25"
|
||||
[circle]="true"
|
||||
[fontSize]="14"
|
||||
[dynamic]="true"
|
||||
[color]="account.value.avatarColor"
|
||||
*ngIf="account.value.email != null"
|
||||
aria-hidden="true"
|
||||
></app-avatar>
|
||||
<div class="accountInfo">
|
||||
<span class="sr-only">{{ "switchAccount" | i18n }}: </span>
|
||||
<span class="email" aria-hidden="true">{{ account.value.email }}</span>
|
||||
<span class="server" aria-hidden="true">
|
||||
<span class="sr-only"> / </span>{{ account.value.server }}
|
||||
</span>
|
||||
<span class="status" aria-hidden="true"
|
||||
><span class="sr-only"> (</span
|
||||
>{{
|
||||
(account.value.authenticationStatus === authStatus.Unlocked ? "unlocked" : "locked")
|
||||
| i18n
|
||||
}}<span class="sr-only">)</span></span
|
||||
>
|
||||
</div>
|
||||
<i
|
||||
class="bwi bwi-2x text-muted"
|
||||
[ngClass]="
|
||||
account.value.authenticationStatus === authStatus.Unlocked ? 'bwi-unlock' : 'bwi-lock'
|
||||
"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
<ng-container *ngIf="activeAccount?.email != null">
|
||||
<div class="border" *ngIf="numberOfAccounts > 0"></div>
|
||||
<ng-container *ngIf="numberOfAccounts < 4">
|
||||
<button type="button" class="add" (click)="addAccount()">
|
||||
<i class="bwi bwi-plus" aria-hidden="true"></i> {{ "addAccount" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="numberOfAccounts === 4">
|
||||
<span class="accountLimitReached">{{ "accountSwitcherLimitReached" | i18n }} </span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="view.activeAccount; else noActiveAccount">
|
||||
<app-avatar
|
||||
[text]="view.activeAccount.name ?? view.activeAccount.email"
|
||||
[id]="view.activeAccount.id"
|
||||
[color]="view.activeAccount.avatarColor"
|
||||
[size]="25"
|
||||
[circle]="true"
|
||||
[fontSize]="14"
|
||||
[dynamic]="true"
|
||||
*ngIf="view.activeAccount.email != null"
|
||||
aria-hidden="true"
|
||||
></app-avatar>
|
||||
<div class="active-account">
|
||||
<div>{{ view.activeAccount.email }}</div>
|
||||
<span>{{ view.activeAccount.server }}</span>
|
||||
<span class="sr-only"> ({{ "switchAccount" | i18n }})</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-template #noActiveAccount>
|
||||
<span>{{ "switchAccount" | i18n }}</span>
|
||||
</ng-template>
|
||||
<i
|
||||
class="bwi"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-angle-down': !isOpen, 'bwi-angle-up': isOpen }"
|
||||
></i>
|
||||
</button>
|
||||
|
||||
<ng-template
|
||||
cdkConnectedOverlay
|
||||
[cdkConnectedOverlayOrigin]="trigger"
|
||||
[cdkConnectedOverlayHasBackdrop]="true"
|
||||
[cdkConnectedOverlayBackdropClass]="'cdk-overlay-transparent-backdrop'"
|
||||
(backdropClick)="close()"
|
||||
(detach)="close()"
|
||||
[cdkConnectedOverlayOpen]="view.showSwitcher && isOpen"
|
||||
[cdkConnectedOverlayPositions]="overlayPosition"
|
||||
cdkConnectedOverlayMinWidth="250px"
|
||||
>
|
||||
<div
|
||||
class="account-switcher-dropdown"
|
||||
[@transformPanel]="'open'"
|
||||
cdkTrapFocus
|
||||
cdkTrapFocusAutoCapture
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<div class="accounts" *ngIf="view.numberOfAccounts > 0">
|
||||
<button
|
||||
*ngFor="let account of view.inactiveAccounts | keyvalue"
|
||||
class="account"
|
||||
(click)="switch(account.key)"
|
||||
>
|
||||
<app-avatar
|
||||
[text]="account.value.name ?? account.value.email"
|
||||
[id]="account.value.id"
|
||||
[size]="25"
|
||||
[circle]="true"
|
||||
[fontSize]="14"
|
||||
[dynamic]="true"
|
||||
[color]="account.value.avatarColor"
|
||||
*ngIf="account.value.email != null"
|
||||
aria-hidden="true"
|
||||
></app-avatar>
|
||||
<div class="accountInfo">
|
||||
<span class="sr-only">{{ "switchAccount" | i18n }}: </span>
|
||||
<span class="email" aria-hidden="true">{{ account.value.email }}</span>
|
||||
<span class="server" aria-hidden="true">
|
||||
<span class="sr-only"> / </span>{{ account.value.server }}
|
||||
</span>
|
||||
<span class="status" aria-hidden="true"
|
||||
><span class="sr-only"> (</span
|
||||
>{{
|
||||
(account.value.authenticationStatus === authStatus.Unlocked ? "unlocked" : "locked")
|
||||
| i18n
|
||||
}}<span class="sr-only">)</span></span
|
||||
>
|
||||
</div>
|
||||
<i
|
||||
class="bwi bwi-2x text-muted"
|
||||
[ngClass]="
|
||||
account.value.authenticationStatus === authStatus.Unlocked ? 'bwi-unlock' : 'bwi-lock'
|
||||
"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
<ng-container *ngIf="view.activeAccount">
|
||||
<div class="border" *ngIf="view.numberOfAccounts > 0"></div>
|
||||
<ng-container *ngIf="view.numberOfAccounts < 4">
|
||||
<button type="button" class="add" (click)="addAccount()">
|
||||
<i class="bwi bwi-plus" aria-hidden="true"></i> {{ "addAccount" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="view.numberOfAccounts === 4">
|
||||
<span class="accountLimitReached">{{ "accountSwitcherLimitReached" | i18n }} </span>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import { animate, state, style, transition, trigger } from "@angular/animations";
|
||||
import { ConnectedPosition } from "@angular/cdk/overlay";
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Component } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { concatMap, firstValueFrom, Subject, takeUntil } from "rxjs";
|
||||
import { combineLatest, firstValueFrom, map, Observable, switchMap } from "rxjs";
|
||||
|
||||
import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { AccountInfo, 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 { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { Account } from "@bitwarden/common/platform/models/domain/account";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
type ActiveAccount = {
|
||||
@@ -52,12 +50,18 @@ type InactiveAccount = ActiveAccount & {
|
||||
]),
|
||||
],
|
||||
})
|
||||
export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
||||
activeAccount?: ActiveAccount;
|
||||
inactiveAccounts: { [userId: string]: InactiveAccount } = {};
|
||||
|
||||
export class AccountSwitcherComponent {
|
||||
activeAccount$: Observable<ActiveAccount | null>;
|
||||
inactiveAccounts$: Observable<{ [userId: string]: InactiveAccount }>;
|
||||
authStatus = AuthenticationStatus;
|
||||
|
||||
view$: Observable<{
|
||||
activeAccount: ActiveAccount | null;
|
||||
inactiveAccounts: { [userId: string]: InactiveAccount };
|
||||
numberOfAccounts: number;
|
||||
showSwitcher: boolean;
|
||||
}>;
|
||||
|
||||
isOpen = false;
|
||||
overlayPosition: ConnectedPosition[] = [
|
||||
{
|
||||
@@ -68,21 +72,9 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
||||
},
|
||||
];
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
showSwitcher$: Observable<boolean>;
|
||||
|
||||
get showSwitcher() {
|
||||
const userIsInAVault = !Utils.isNullOrWhitespace(this.activeAccount?.email);
|
||||
const userIsAddingAnAdditionalAccount = Object.keys(this.inactiveAccounts).length > 0;
|
||||
return userIsInAVault || userIsAddingAnAdditionalAccount;
|
||||
}
|
||||
|
||||
get numberOfAccounts() {
|
||||
if (this.inactiveAccounts == null) {
|
||||
this.isOpen = false;
|
||||
return 0;
|
||||
}
|
||||
return Object.keys(this.inactiveAccounts).length;
|
||||
}
|
||||
numberOfAccounts$: Observable<number>;
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
@@ -90,37 +82,65 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
||||
private avatarService: AvatarService,
|
||||
private messagingService: MessagingService,
|
||||
private router: Router,
|
||||
private tokenService: TokenService,
|
||||
private environmentService: EnvironmentService,
|
||||
private loginEmailService: LoginEmailServiceAbstraction,
|
||||
) {}
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
this.activeAccount$ = this.accountService.activeAccount$.pipe(
|
||||
switchMap(async (active) => {
|
||||
if (active == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.stateService.accounts$
|
||||
.pipe(
|
||||
concatMap(async (accounts: { [userId: string]: Account }) => {
|
||||
this.inactiveAccounts = await this.createInactiveAccounts(accounts);
|
||||
return {
|
||||
id: active.id,
|
||||
name: active.name,
|
||||
email: active.email,
|
||||
avatarColor: await firstValueFrom(this.avatarService.avatarColor$),
|
||||
server: (await this.environmentService.getEnvironment())?.getHostname(),
|
||||
};
|
||||
}),
|
||||
);
|
||||
this.inactiveAccounts$ = combineLatest([
|
||||
this.activeAccount$,
|
||||
this.accountService.accounts$,
|
||||
this.authService.authStatuses$,
|
||||
]).pipe(
|
||||
switchMap(async ([activeAccount, accounts, accountStatuses]) => {
|
||||
// Filter out logged out accounts and active account
|
||||
accounts = Object.fromEntries(
|
||||
Object.entries(accounts).filter(
|
||||
([id]: [UserId, AccountInfo]) =>
|
||||
accountStatuses[id] !== AuthenticationStatus.LoggedOut || id === activeAccount?.id,
|
||||
),
|
||||
);
|
||||
return this.createInactiveAccounts(accounts);
|
||||
}),
|
||||
);
|
||||
this.showSwitcher$ = combineLatest([this.activeAccount$, this.inactiveAccounts$]).pipe(
|
||||
map(([activeAccount, inactiveAccounts]) => {
|
||||
const hasActiveUser = activeAccount != null;
|
||||
const userIsAddingAnAdditionalAccount = Object.keys(inactiveAccounts).length > 0;
|
||||
return hasActiveUser || userIsAddingAnAdditionalAccount;
|
||||
}),
|
||||
);
|
||||
this.numberOfAccounts$ = this.inactiveAccounts$.pipe(
|
||||
map((accounts) => Object.keys(accounts).length),
|
||||
);
|
||||
|
||||
try {
|
||||
this.activeAccount = {
|
||||
id: await this.tokenService.getUserId(),
|
||||
name: (await this.tokenService.getName()) ?? (await this.tokenService.getEmail()),
|
||||
email: await this.tokenService.getEmail(),
|
||||
avatarColor: await firstValueFrom(this.avatarService.avatarColor$),
|
||||
server: (await this.environmentService.getEnvironment())?.getHostname(),
|
||||
};
|
||||
} catch {
|
||||
this.activeAccount = undefined;
|
||||
}
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
this.view$ = combineLatest([
|
||||
this.activeAccount$,
|
||||
this.inactiveAccounts$,
|
||||
this.numberOfAccounts$,
|
||||
this.showSwitcher$,
|
||||
]).pipe(
|
||||
map(([activeAccount, inactiveAccounts, numberOfAccounts, showSwitcher]) => ({
|
||||
activeAccount,
|
||||
inactiveAccounts,
|
||||
numberOfAccounts,
|
||||
showSwitcher,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
toggle() {
|
||||
@@ -144,11 +164,13 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
||||
await this.loginEmailService.saveEmailSettings();
|
||||
|
||||
await this.router.navigate(["/login"]);
|
||||
await this.stateService.setActiveUser(null);
|
||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
await this.stateService.clearDecryptedData(activeAccount?.id as UserId);
|
||||
await this.accountService.switchAccount(null);
|
||||
}
|
||||
|
||||
private async createInactiveAccounts(baseAccounts: {
|
||||
[userId: string]: Account;
|
||||
[userId: string]: AccountInfo;
|
||||
}): Promise<{ [userId: string]: InactiveAccount }> {
|
||||
const inactiveAccounts: { [userId: string]: InactiveAccount } = {};
|
||||
|
||||
@@ -159,8 +181,8 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
||||
|
||||
inactiveAccounts[userId] = {
|
||||
id: userId,
|
||||
name: baseAccounts[userId].profile.name,
|
||||
email: baseAccounts[userId].profile.email,
|
||||
name: baseAccounts[userId].name,
|
||||
email: baseAccounts[userId].email,
|
||||
authenticationStatus: await this.authService.getAuthStatus(userId),
|
||||
avatarColor: await firstValueFrom(this.avatarService.getUserAvatarColor$(userId as UserId)),
|
||||
server: (await this.environmentService.getEnvironment(userId))?.getHostname(),
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { UntypedFormControl } from "@angular/forms";
|
||||
import { Subscription } from "rxjs";
|
||||
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
|
||||
import { SearchBarService, SearchBarState } from "./search-bar.service";
|
||||
|
||||
@@ -18,7 +18,7 @@ export class SearchComponent implements OnInit, OnDestroy {
|
||||
|
||||
constructor(
|
||||
private searchBarService: SearchBarService,
|
||||
private stateService: StateService,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
this.searchBarService.state$.subscribe((state) => {
|
||||
@@ -33,7 +33,7 @@ export class SearchComponent implements OnInit, OnDestroy {
|
||||
|
||||
ngOnInit() {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
this.activeAccountSubscription = this.stateService.activeAccount$.subscribe((value) => {
|
||||
this.activeAccountSubscription = this.accountService.activeAccount$.subscribe((_) => {
|
||||
this.searchBarService.setSearchText("");
|
||||
this.searchText.patchValue("");
|
||||
});
|
||||
|
||||
@@ -59,7 +59,6 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/ge
|
||||
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { LoginGuard } from "../../auth/guards/login.guard";
|
||||
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
|
||||
import { Account } from "../../models/account";
|
||||
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
|
||||
@@ -102,7 +101,6 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider(InitService),
|
||||
safeProvider(NativeMessagingService),
|
||||
safeProvider(SearchBarService),
|
||||
safeProvider(LoginGuard),
|
||||
safeProvider(DialogService),
|
||||
safeProvider({
|
||||
provide: APP_INITIALIZER as SafeInjectionToken<() => void>,
|
||||
@@ -192,6 +190,7 @@ const safeProviders: SafeProvider[] = [
|
||||
AutofillSettingsServiceAbstraction,
|
||||
VaultTimeoutSettingsService,
|
||||
BiometricStateService,
|
||||
AccountServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ActivatedRoute } from "@angular/router";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@@ -59,6 +60,10 @@ describe("GeneratorComponent", () => {
|
||||
provide: CipherService,
|
||||
useValue: mock<CipherService>(),
|
||||
},
|
||||
{
|
||||
provide: AccountService,
|
||||
useValue: mock<AccountService>(),
|
||||
},
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
|
||||
@@ -4,6 +4,7 @@ import { FormBuilder } from "@angular/forms";
|
||||
|
||||
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/tools/send/add-edit.component";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -34,6 +35,7 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||
dialogService: DialogService,
|
||||
formBuilder: FormBuilder,
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
accountService: AccountService,
|
||||
) {
|
||||
super(
|
||||
i18nService,
|
||||
@@ -49,6 +51,7 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||
dialogService,
|
||||
formBuilder,
|
||||
billingAccountProfileStateService,
|
||||
accountService,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { CanActivate } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
|
||||
const maxAllowedAccounts = 5;
|
||||
|
||||
@Injectable()
|
||||
export class LoginGuard implements CanActivate {
|
||||
protected homepage = "vault";
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
async canActivate() {
|
||||
const accounts = await firstValueFrom(this.stateService.accounts$);
|
||||
if (accounts != null && Object.keys(accounts).length >= maxAllowedAccounts) {
|
||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("accountLimitReached"));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
38
apps/desktop/src/auth/guards/max-accounts.guard.ts
Normal file
38
apps/desktop/src/auth/guards/max-accounts.guard.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { inject } from "@angular/core";
|
||||
import { CanActivateFn } from "@angular/router";
|
||||
import { Observable, map } from "rxjs";
|
||||
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
const maxAllowedAccounts = 5;
|
||||
|
||||
function maxAccountsGuard(): Observable<boolean> {
|
||||
const authService = inject(AuthService);
|
||||
const toastService = inject(ToastService);
|
||||
const i18nService = inject(I18nService);
|
||||
|
||||
return authService.authStatuses$.pipe(
|
||||
map((statuses) =>
|
||||
Object.values(statuses).filter((status) => status != AuthenticationStatus.LoggedOut),
|
||||
),
|
||||
map((accounts) => {
|
||||
if (accounts != null && Object.keys(accounts).length >= maxAllowedAccounts) {
|
||||
toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: i18nService.t("accountLimitReached"),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function maxAccountsGuardFn(): CanActivateFn {
|
||||
return () => maxAccountsGuard();
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeou
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
@@ -50,7 +51,7 @@ describe("LockComponent", () => {
|
||||
let component: LockComponent;
|
||||
let fixture: ComponentFixture<LockComponent>;
|
||||
let stateServiceMock: MockProxy<StateService>;
|
||||
const biometricStateService = mock<BiometricStateService>();
|
||||
let biometricStateService: MockProxy<BiometricStateService>;
|
||||
let messagingServiceMock: MockProxy<MessagingService>;
|
||||
let broadcasterServiceMock: MockProxy<BroadcasterService>;
|
||||
let platformUtilsServiceMock: MockProxy<PlatformUtilsService>;
|
||||
@@ -62,7 +63,6 @@ describe("LockComponent", () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
stateServiceMock = mock<StateService>();
|
||||
stateServiceMock.activeAccount$ = of(null);
|
||||
|
||||
messagingServiceMock = mock<MessagingService>();
|
||||
broadcasterServiceMock = mock<BroadcasterService>();
|
||||
@@ -73,6 +73,7 @@ describe("LockComponent", () => {
|
||||
|
||||
mockMasterPasswordService = new FakeMasterPasswordService();
|
||||
|
||||
biometricStateService = mock();
|
||||
biometricStateService.dismissedRequirePasswordOnStartCallout$ = of(false);
|
||||
biometricStateService.promptAutomatically$ = of(false);
|
||||
biometricStateService.promptCancelled$ = of(false);
|
||||
@@ -165,6 +166,10 @@ describe("LockComponent", () => {
|
||||
provide: AccountService,
|
||||
useValue: accountService,
|
||||
},
|
||||
{
|
||||
provide: AuthService,
|
||||
useValue: mock(),
|
||||
},
|
||||
{
|
||||
provide: KdfConfigService,
|
||||
useValue: mock<KdfConfigService>(),
|
||||
|
||||
@@ -10,6 +10,7 @@ import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeou
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
@@ -64,6 +65,7 @@ export class LockComponent extends BaseLockComponent {
|
||||
pinCryptoService: PinCryptoServiceAbstraction,
|
||||
biometricStateService: BiometricStateService,
|
||||
accountService: AccountService,
|
||||
authService: AuthService,
|
||||
kdfConfigService: KdfConfigService,
|
||||
) {
|
||||
super(
|
||||
@@ -89,6 +91,7 @@ export class LockComponent extends BaseLockComponent {
|
||||
pinCryptoService,
|
||||
biometricStateService,
|
||||
accountService,
|
||||
authService,
|
||||
kdfConfigService,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -65,9 +65,10 @@ export class Menubar {
|
||||
isLocked = updateRequest.accounts[updateRequest.activeUserId]?.isLocked ?? true;
|
||||
}
|
||||
|
||||
const isLockable = !isLocked && updateRequest?.accounts[updateRequest.activeUserId]?.isLockable;
|
||||
const isLockable =
|
||||
!isLocked && updateRequest?.accounts?.[updateRequest.activeUserId]?.isLockable;
|
||||
const hasMasterPassword =
|
||||
updateRequest?.accounts[updateRequest.activeUserId]?.hasMasterPassword ?? false;
|
||||
updateRequest?.accounts?.[updateRequest.activeUserId]?.hasMasterPassword ?? false;
|
||||
|
||||
this.items = [
|
||||
new FileMenu(
|
||||
|
||||
@@ -2,7 +2,7 @@ import { DOCUMENT } from "@angular/common";
|
||||
import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||
import { NavigationEnd, Router } from "@angular/router";
|
||||
import * as jq from "jquery";
|
||||
import { Subject, switchMap, takeUntil, timer } from "rxjs";
|
||||
import { Subject, firstValueFrom, map, switchMap, takeUntil, timer } from "rxjs";
|
||||
|
||||
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
|
||||
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
||||
@@ -10,6 +10,7 @@ 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 { 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";
|
||||
import { PaymentMethodWarningsServiceAbstraction as PaymentMethodWarningService } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction";
|
||||
@@ -51,7 +52,7 @@ const PaymentMethodWarningsRefresh = 60000; // 1 Minute
|
||||
templateUrl: "app.component.html",
|
||||
})
|
||||
export class AppComponent implements OnDestroy, OnInit {
|
||||
private lastActivity: number = null;
|
||||
private lastActivity: Date = null;
|
||||
private idleTimer: number = null;
|
||||
private isIdle = false;
|
||||
private destroy$ = new Subject<void>();
|
||||
@@ -86,6 +87,7 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
private stateEventRunnerService: StateEventRunnerService,
|
||||
private paymentMethodWarningService: PaymentMethodWarningService,
|
||||
private organizationService: InternalOrganizationServiceAbstraction,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -298,15 +300,16 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
private async recordActivity() {
|
||||
const now = new Date().getTime();
|
||||
if (this.lastActivity != null && now - this.lastActivity < 250) {
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const now = new Date();
|
||||
if (this.lastActivity != null && now.getTime() - this.lastActivity.getTime() < 250) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastActivity = now;
|
||||
// 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.stateService.setLastActive(now);
|
||||
await this.accountService.setAccountActivity(activeUserId, now);
|
||||
// Idle states
|
||||
if (this.isIdle) {
|
||||
this.isIdle = false;
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
[bitMenuTriggerFor]="accountMenu"
|
||||
class="tw-border-0 tw-bg-transparent tw-p-0"
|
||||
>
|
||||
<dynamic-avatar [id]="account.userId" [text]="account | userName"></dynamic-avatar>
|
||||
<dynamic-avatar [id]="account.id" [text]="account | userName"></dynamic-avatar>
|
||||
</button>
|
||||
|
||||
<bit-menu #accountMenu>
|
||||
@@ -67,7 +67,7 @@
|
||||
class="tw-flex tw-items-center tw-px-4 tw-py-1 tw-leading-tight tw-text-info"
|
||||
appStopProp
|
||||
>
|
||||
<dynamic-avatar [id]="account.userId" [text]="account | userName"></dynamic-avatar>
|
||||
<dynamic-avatar [id]="account.id" [text]="account | userName"></dynamic-avatar>
|
||||
<div class="tw-ml-2 tw-block tw-overflow-hidden tw-whitespace-nowrap">
|
||||
<span>{{ "loggedInAs" | i18n }}</span>
|
||||
<small class="tw-block tw-overflow-hidden tw-whitespace-nowrap tw-text-muted">
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { combineLatest, map, Observable } from "rxjs";
|
||||
import { map, Observable } from "rxjs";
|
||||
|
||||
import { User } from "@bitwarden/angular/pipes/user-name.pipe";
|
||||
import { UnassignedItemsBannerService } from "@bitwarden/angular/services/unassigned-items-banner.service";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { AccountProfile } from "@bitwarden/common/platform/models/domain/account";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
@Component({
|
||||
selector: "app-header",
|
||||
@@ -28,7 +29,7 @@ export class WebHeaderComponent {
|
||||
@Input() icon: string;
|
||||
|
||||
protected routeData$: Observable<{ titleId: string }>;
|
||||
protected account$: Observable<AccountProfile>;
|
||||
protected account$: Observable<User & { id: UserId }>;
|
||||
protected canLock$: Observable<boolean>;
|
||||
protected selfHosted: boolean;
|
||||
protected hostname = location.hostname;
|
||||
@@ -38,12 +39,12 @@ export class WebHeaderComponent {
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private stateService: StateService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||
private messagingService: MessagingService,
|
||||
protected unassignedItemsBannerService: UnassignedItemsBannerService,
|
||||
private configService: ConfigService,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
this.routeData$ = this.route.data.pipe(
|
||||
map((params) => {
|
||||
@@ -55,14 +56,7 @@ export class WebHeaderComponent {
|
||||
|
||||
this.selfHosted = this.platformUtilsService.isSelfHost();
|
||||
|
||||
this.account$ = combineLatest([
|
||||
this.stateService.activeAccount$,
|
||||
this.stateService.accounts$,
|
||||
]).pipe(
|
||||
map(([activeAccount, accounts]) => {
|
||||
return accounts[activeAccount]?.profile;
|
||||
}),
|
||||
);
|
||||
this.account$ = this.accountService.activeAccount$;
|
||||
this.canLock$ = this.vaultTimeoutSettingsService
|
||||
.availableVaultTimeoutActions$()
|
||||
.pipe(map((actions) => actions.includes(VaultTimeoutAction.Lock)));
|
||||
|
||||
@@ -5,6 +5,7 @@ import { FormBuilder } from "@angular/forms";
|
||||
|
||||
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/tools/send/add-edit.component";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -40,6 +41,7 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
protected dialogRef: DialogRef,
|
||||
@Inject(DIALOG_DATA) params: { sendId: string },
|
||||
accountService: AccountService,
|
||||
) {
|
||||
super(
|
||||
i18nService,
|
||||
@@ -55,6 +57,7 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||
dialogService,
|
||||
formBuilder,
|
||||
billingAccountProfileStateService,
|
||||
accountService,
|
||||
);
|
||||
|
||||
this.sendId = params.sendId;
|
||||
|
||||
@@ -55,7 +55,6 @@ export default {
|
||||
{
|
||||
provide: StateService,
|
||||
useValue: {
|
||||
activeAccount$: new BehaviorSubject("1").asObservable(),
|
||||
accounts$: new BehaviorSubject({ "1": { profile: { name: "Foo" } } }).asObservable(),
|
||||
async getShowFavicon() {
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user