Fix active account and searchBar observables/subscriptions (#3268)

* Change subscription to rely on observables and not on BehaviourSubject

* Ensure OnDestroy is added to AppComponent

* Fix check for no active accounts to redirect to the login page instead of lock

* Change subscription handling on SearchBarService

* Fix naming convention: Observables should have a $ suffix

* Remove obsolete linter hint

* Fix activeAccountUnlocked getting exposed as Observable but is instantiated as BehaviourSubject
This commit is contained in:
Daniel James Smith 2022-08-09 21:11:51 +02:00 committed by GitHub
parent c4f9c2cca6
commit cfc8858ef9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 52 additions and 37 deletions

View File

@ -56,7 +56,7 @@ export class AppComponent implements OnInit, OnDestroy {
// Clear them aggressively to make sure this doesn't occur // Clear them aggressively to make sure this doesn't occur
await this.clearComponentStates(); await this.clearComponentStates();
this.stateService.activeAccount.pipe(takeUntil(this.destroy$)).subscribe((userId) => { this.stateService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((userId) => {
this.activeUserId = userId; this.activeUserId = userId;
}); });
@ -84,7 +84,7 @@ export class AppComponent implements OnInit, OnDestroy {
}); });
} }
if (this.stateService.activeAccount.getValue() == null) { if (this.activeUserId === null) {
this.router.navigate(["home"]); this.router.navigate(["home"]);
} }
}); });

View File

@ -1,4 +1,4 @@
import { Component, NgZone, OnDestroy } from "@angular/core"; import { Component, NgZone } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { ipcRenderer } from "electron"; import { ipcRenderer } from "electron";
@ -22,7 +22,7 @@ const BroadcasterSubscriptionId = "LockComponent";
selector: "app-lock", selector: "app-lock",
templateUrl: "lock.component.html", templateUrl: "lock.component.html",
}) })
export class LockComponent extends BaseLockComponent implements OnDestroy { export class LockComponent extends BaseLockComponent {
private deferFocus: boolean = null; private deferFocus: boolean = null;
authenicatedUrl = "vault"; authenicatedUrl = "vault";
unAuthenicatedUrl = "update-temp-password"; unAuthenicatedUrl = "update-temp-password";
@ -103,6 +103,7 @@ export class LockComponent extends BaseLockComponent implements OnDestroy {
} }
ngOnDestroy() { ngOnDestroy() {
super.ngOnDestroy();
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
} }

View File

@ -1,6 +1,7 @@
import { import {
Component, Component,
NgZone, NgZone,
OnDestroy,
OnInit, OnInit,
SecurityContext, SecurityContext,
Type, Type,
@ -77,7 +78,7 @@ const systemTimeoutOptions = {
</div> </div>
`, `,
}) })
export class AppComponent implements OnInit { export class AppComponent implements OnInit, OnDestroy {
@ViewChild("settings", { read: ViewContainerRef, static: true }) settingsRef: ViewContainerRef; @ViewChild("settings", { read: ViewContainerRef, static: true }) settingsRef: ViewContainerRef;
@ViewChild("premium", { read: ViewContainerRef, static: true }) premiumRef: ViewContainerRef; @ViewChild("premium", { read: ViewContainerRef, static: true }) premiumRef: ViewContainerRef;
@ViewChild("passwordHistory", { read: ViewContainerRef, static: true }) @ViewChild("passwordHistory", { read: ViewContainerRef, static: true })
@ -129,7 +130,7 @@ export class AppComponent implements OnInit {
) {} ) {}
ngOnInit() { ngOnInit() {
this.stateService.activeAccount.pipe(takeUntil(this.destroy$)).subscribe((userId) => { this.stateService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((userId) => {
this.activeUserId = userId; this.activeUserId = userId;
}); });

View File

@ -8,15 +8,16 @@ export type SearchBarState = {
@Injectable() @Injectable()
export class SearchBarService { export class SearchBarService {
searchText = new BehaviorSubject<string>(null); private searchTextSubject = new BehaviorSubject<string>(null);
searchText$ = this.searchTextSubject.asObservable();
private _state = { private _state = {
enabled: false, enabled: false,
placeholderText: "", placeholderText: "",
}; };
// tslint:disable-next-line:member-ordering private stateSubject = new BehaviorSubject<SearchBarState>(this._state);
state = new BehaviorSubject<SearchBarState>(this._state); state$ = this.stateSubject.asObservable();
setEnabled(enabled: boolean) { setEnabled(enabled: boolean) {
this._state.enabled = enabled; this._state.enabled = enabled;
@ -29,10 +30,10 @@ export class SearchBarService {
} }
setSearchText(value: string) { setSearchText(value: string) {
this.searchText.next(value); this.searchTextSubject.next(value);
} }
private updateState() { private updateState() {
this.state.next(this._state); this.stateSubject.next(this._state);
} }
} }

View File

@ -1,5 +1,6 @@
import { Component, OnDestroy, OnInit } from "@angular/core"; import { Component, OnDestroy, OnInit } from "@angular/core";
import { UntypedFormControl } from "@angular/forms"; import { UntypedFormControl } from "@angular/forms";
import { Subscription } from "rxjs";
import { StateService } from "@bitwarden/common/abstractions/state.service"; import { StateService } from "@bitwarden/common/abstractions/state.service";
@ -13,8 +14,10 @@ export class SearchComponent implements OnInit, OnDestroy {
state: SearchBarState; state: SearchBarState;
searchText: UntypedFormControl = new UntypedFormControl(null); searchText: UntypedFormControl = new UntypedFormControl(null);
private activeAccountSubscription: Subscription;
constructor(private searchBarService: SearchBarService, private stateService: StateService) { constructor(private searchBarService: SearchBarService, private stateService: StateService) {
this.searchBarService.state.subscribe((state) => { this.searchBarService.state$.subscribe((state) => {
this.state = state; this.state = state;
}); });
@ -24,13 +27,13 @@ export class SearchComponent implements OnInit, OnDestroy {
} }
ngOnInit() { ngOnInit() {
this.stateService.activeAccount.subscribe((value) => { this.activeAccountSubscription = this.stateService.activeAccount$.subscribe((value) => {
this.searchBarService.setSearchText(""); this.searchBarService.setSearchText("");
this.searchText.patchValue(""); this.searchText.patchValue("");
}); });
} }
ngOnDestroy() { ngOnDestroy() {
this.stateService.activeAccount.unsubscribe(); this.activeAccountSubscription.unsubscribe();
} }
} }

View File

@ -56,7 +56,7 @@ export class SendComponent extends BaseSendComponent implements OnInit, OnDestro
policyService, policyService,
logService logService
); );
this.searchBarService.searchText.subscribe((searchText) => { this.searchBarService.searchText$.subscribe((searchText) => {
this.searchText = searchText; this.searchText = searchText;
this.searchTextChanged(); this.searchTextChanged();
}); });

View File

@ -14,7 +14,7 @@ export class CiphersComponent extends BaseCiphersComponent {
constructor(searchService: SearchService, searchBarService: SearchBarService) { constructor(searchService: SearchService, searchBarService: SearchBarService) {
super(searchService); super(searchService);
searchBarService.searchText.subscribe((searchText) => { searchBarService.searchText$.subscribe((searchText) => {
this.searchText = searchText; this.searchText = searchText;
this.search(200); this.search(200);
}); });

View File

@ -1,5 +1,6 @@
import { Directive, NgZone, OnInit } from "@angular/core"; import { Directive, NgZone, OnDestroy, OnInit } from "@angular/core";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { Subscription } from "rxjs";
import { take } from "rxjs/operators"; import { take } from "rxjs/operators";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
@ -20,7 +21,7 @@ import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCry
import { SecretVerificationRequest } from "@bitwarden/common/models/request/secretVerificationRequest"; import { SecretVerificationRequest } from "@bitwarden/common/models/request/secretVerificationRequest";
@Directive() @Directive()
export class LockComponent implements OnInit { export class LockComponent implements OnInit, OnDestroy {
masterPassword = ""; masterPassword = "";
pin = ""; pin = "";
showPassword = false; showPassword = false;
@ -39,6 +40,8 @@ export class LockComponent implements OnInit {
private invalidPinAttempts = 0; private invalidPinAttempts = 0;
private pinSet: [boolean, boolean]; private pinSet: [boolean, boolean];
private activeAccountSubscription: Subscription;
constructor( constructor(
protected router: Router, protected router: Router,
protected i18nService: I18nService, protected i18nService: I18nService,
@ -57,11 +60,15 @@ export class LockComponent implements OnInit {
async ngOnInit() { async ngOnInit() {
// Load the first and observe updates // Load the first and observe updates
await this.load(); await this.load();
this.stateService.activeAccount.subscribe(async () => { this.activeAccountSubscription = this.stateService.activeAccount$.subscribe(async () => {
await this.load(); await this.load();
}); });
} }
ngOnDestroy() {
this.activeAccountSubscription.unsubscribe();
}
async submit() { async submit() {
if (this.pinLock && (this.pin == null || this.pin === "")) { if (this.pinLock && (this.pin == null || this.pin === "")) {
this.platformUtilsService.showToast( this.platformUtilsService.showToast(

View File

@ -32,7 +32,7 @@ describe("Folder Service", () => {
stateService.getEncryptedFolders().resolves({ stateService.getEncryptedFolders().resolves({
"1": folderData("1", "test"), "1": folderData("1", "test"),
}); });
stateService.activeAccount.returns(activeAccount); stateService.activeAccount$.returns(activeAccount);
stateService.activeAccountUnlocked.returns(activeAccountUnlocked); stateService.activeAccountUnlocked.returns(activeAccountUnlocked);
(window as any).bitwardenContainerService = new ContainerService(cryptoService); (window as any).bitwardenContainerService = new ContainerService(cryptoService);

View File

@ -26,9 +26,8 @@ import { SendView } from "../models/view/sendView";
export abstract class StateService<T extends Account = Account> { export abstract class StateService<T extends Account = Account> {
accounts: BehaviorSubject<{ [userId: string]: T }>; accounts: BehaviorSubject<{ [userId: string]: T }>;
activeAccount: BehaviorSubject<string>; activeAccount$: Observable<string>;
activeAccountUnlocked$: Observable<boolean>;
activeAccountUnlocked: Observable<boolean>;
addAccount: (account: T) => Promise<void>; addAccount: (account: T) => Promise<void>;
setActiveUser: (userId: string) => Promise<void>; setActiveUser: (userId: string) => Promise<void>;

View File

@ -22,7 +22,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
private scimUrl: string = null; private scimUrl: string = null;
constructor(private stateService: StateService) { constructor(private stateService: StateService) {
this.stateService.activeAccount.subscribe(async () => { this.stateService.activeAccount$.subscribe(async () => {
await this.setUrlsFromStorage(); await this.setUrlsFromStorage();
}); });
} }

View File

@ -25,7 +25,7 @@ export class FolderService implements InternalFolderServiceAbstraction {
private cipherService: CipherService, private cipherService: CipherService,
private stateService: StateService private stateService: StateService
) { ) {
this.stateService.activeAccountUnlocked.subscribe(async (unlocked) => { this.stateService.activeAccountUnlocked$.subscribe(async (unlocked) => {
if ((Utils.global as any).bitwardenContainerService == null) { if ((Utils.global as any).bitwardenContainerService == null) {
return; return;
} }

View File

@ -55,8 +55,11 @@ export class StateService<
> implements StateServiceAbstraction<TAccount> > implements StateServiceAbstraction<TAccount>
{ {
accounts = new BehaviorSubject<{ [userId: string]: TAccount }>({}); accounts = new BehaviorSubject<{ [userId: string]: TAccount }>({});
activeAccount = new BehaviorSubject<string>(null); private activeAccountSubject = new BehaviorSubject<string>(null);
activeAccountUnlocked = new BehaviorSubject<boolean>(false); activeAccount$ = this.activeAccountSubject.asObservable();
private activeAccountUnlockedSubject = new BehaviorSubject<boolean>(false);
activeAccountUnlocked$ = this.activeAccountUnlockedSubject.asObservable();
private hasBeenInited = false; private hasBeenInited = false;
private isRecoveredSession = false; private isRecoveredSession = false;
@ -73,17 +76,17 @@ export class StateService<
protected useAccountCache: boolean = true protected useAccountCache: boolean = true
) { ) {
// If the account gets changed, verify the new account is unlocked // If the account gets changed, verify the new account is unlocked
this.activeAccount.subscribe(async (userId) => { this.activeAccountSubject.subscribe(async (userId) => {
if (userId == null && this.activeAccountUnlocked.getValue() == false) { if (userId == null && this.activeAccountUnlockedSubject.getValue() == false) {
return; return;
} else if (userId == null) { } else if (userId == null) {
this.activeAccountUnlocked.next(false); this.activeAccountUnlockedSubject.next(false);
} }
// FIXME: This should be refactored into AuthService or a similar service, // FIXME: This should be refactored into AuthService or a similar service,
// as checking for the existance of the crypto key is a low level // as checking for the existance of the crypto key is a low level
// implementation detail. // implementation detail.
this.activeAccountUnlocked.next((await this.getCryptoMasterKey()) != null); this.activeAccountUnlockedSubject.next((await this.getCryptoMasterKey()) != null);
}); });
} }
@ -125,7 +128,7 @@ export class StateService<
state.activeUserId = storedActiveUser; state.activeUserId = storedActiveUser;
} }
await this.pushAccounts(); await this.pushAccounts();
this.activeAccount.next(state.activeUserId); this.activeAccountSubject.next(state.activeUserId);
return state; return state;
}); });
@ -154,7 +157,7 @@ export class StateService<
await this.scaffoldNewAccountStorage(account); await this.scaffoldNewAccountStorage(account);
await this.setLastActive(new Date().getTime(), { userId: account.profile.userId }); await this.setLastActive(new Date().getTime(), { userId: account.profile.userId });
await this.setActiveUser(account.profile.userId); await this.setActiveUser(account.profile.userId);
this.activeAccount.next(account.profile.userId); this.activeAccountSubject.next(account.profile.userId);
} }
async setActiveUser(userId: string): Promise<void> { async setActiveUser(userId: string): Promise<void> {
@ -162,7 +165,7 @@ export class StateService<
await this.updateState(async (state) => { await this.updateState(async (state) => {
state.activeUserId = userId; state.activeUserId = userId;
await this.storageService.save(keys.activeUserId, userId); await this.storageService.save(keys.activeUserId, userId);
this.activeAccount.next(state.activeUserId); this.activeAccountSubject.next(state.activeUserId);
return state; return state;
}); });
@ -497,12 +500,12 @@ export class StateService<
this.reconcileOptions(options, await this.defaultInMemoryOptions()) this.reconcileOptions(options, await this.defaultInMemoryOptions())
); );
if (options.userId == this.activeAccount.getValue()) { if (options.userId == this.activeAccountSubject.getValue()) {
const nextValue = value != null; const nextValue = value != null;
// Avoid emitting if we are already unlocked // Avoid emitting if we are already unlocked
if (this.activeAccountUnlocked.getValue() != nextValue) { if (this.activeAccountUnlockedSubject.getValue() != nextValue) {
this.activeAccountUnlocked.next(nextValue); this.activeAccountUnlockedSubject.next(nextValue);
} }
} }
} }