1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 07:43:35 +00:00

[PM-12045] search service activeuserstate (#13035)

* removing activeuserstate from search service
This commit is contained in:
Jason Ng
2025-03-06 12:26:24 -05:00
committed by GitHub
parent 9761588a2a
commit f65daf7284
19 changed files with 159 additions and 637 deletions

View File

@@ -389,11 +389,14 @@ export class Fido2Component implements OnInit, OnDestroy {
}
protected async search() {
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
this.hasSearched = true;
const isSearchable = await this.searchService.isSearchable(this.searchText);
const isSearchable = await this.searchService.isSearchable(userId, this.searchText);
if (isSearchable) {
this.displayedCiphers = await this.searchService.searchCiphers(
userId,
this.searchText,
null,
this.ciphers,

View File

@@ -87,7 +87,7 @@ describe("VaultPopupItemsService", () => {
failedToDecryptCiphersSubject.asObservable(),
);
searchService.searchCiphers.mockImplementation(async (_, __, ciphers) => ciphers);
searchService.searchCiphers.mockImplementation(async (userId, _, __, ciphers) => ciphers);
cipherServiceMock.filterCiphersForUrl.mockImplementation(async (ciphers) =>
ciphers.filter((c) => ["0", "1"].includes(c.id)),
);
@@ -276,7 +276,7 @@ describe("VaultPopupItemsService", () => {
it("should filter autoFillCiphers$ down to search term", (done) => {
const searchText = "Login";
searchService.searchCiphers.mockImplementation(async (q, _, ciphers) => {
searchService.searchCiphers.mockImplementation(async (userId, q, _, ciphers) => {
return ciphers.filter((cipher) => {
return cipher.name.includes(searchText);
});
@@ -472,7 +472,12 @@ describe("VaultPopupItemsService", () => {
service.applyFilter(searchText);
service.favoriteCiphers$.subscribe(() => {
expect(searchServiceSpy).toHaveBeenCalledWith(searchText, undefined, expect.anything());
expect(searchServiceSpy).toHaveBeenCalledWith(
"UserId",
searchText,
undefined,
expect.anything(),
);
done();
});
});

View File

@@ -23,6 +23,7 @@ import { CollectionService } from "@bitwarden/admin-console/common";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SyncService } from "@bitwarden/common/platform/sync";
import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
@@ -138,22 +139,29 @@ export class VaultPopupItemsService {
* Observable that indicates whether there is search text present that is searchable.
* @private
*/
private _hasSearchText = this.searchText$.pipe(
switchMap((searchText) => this.searchService.isSearchable(searchText)),
private _hasSearchText = combineLatest([
this.searchText$,
getUserId(this.accountService.activeAccount$),
]).pipe(
switchMap(([searchText, userId]) => {
return this.searchService.isSearchable(userId, searchText);
}),
);
private _filteredCipherList$: Observable<PopupCipherView[]> = combineLatest([
this._activeCipherList$,
this.searchText$,
this.vaultPopupListFiltersService.filterFunction$,
getUserId(this.accountService.activeAccount$),
]).pipe(
map(([ciphers, searchText, filterFunction]): [CipherView[], string] => [
map(([ciphers, searchText, filterFunction, userId]): [CipherView[], string, UserId] => [
filterFunction(ciphers),
searchText,
userId,
]),
switchMap(
([ciphers, searchText]) =>
this.searchService.searchCiphers(searchText, undefined, ciphers) as Promise<
([ciphers, searchText, userId]) =>
this.searchService.searchCiphers(userId, searchText, undefined, ciphers) as Promise<
PopupCipherView[]
>,
),

View File

@@ -5,6 +5,7 @@ import { Component, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { SendComponent as BaseSendComponent } from "@bitwarden/angular/tools/send/send.component";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -52,6 +53,7 @@ export class SendComponent extends BaseSendComponent implements OnInit, OnDestro
sendApiService: SendApiService,
dialogService: DialogService,
toastService: ToastService,
accountService: AccountService,
) {
super(
sendService,
@@ -65,6 +67,7 @@ export class SendComponent extends BaseSendComponent implements OnInit, OnDestro
sendApiService,
dialogService,
toastService,
accountService,
);
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
this.searchBarService.searchText$.subscribe((searchText) => {

View File

@@ -1,425 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, ViewChild, ViewContainerRef } from "@angular/core";
import { FormControl } from "@angular/forms";
import { firstValueFrom, concatMap, map, lastValueFrom, startWith, debounceTime } from "rxjs";
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service";
import {
OrganizationUserStatusType,
OrganizationUserType,
ProviderUserStatusType,
ProviderUserType,
} from "@bitwarden/common/admin-console/enums";
import { ProviderUserUserDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user.response";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
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";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { DialogService, ToastService } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
import { OrganizationUserView } from "../organizations/core/views/organization-user.view";
import { UserConfirmComponent } from "../organizations/manage/user-confirm.component";
type StatusType = OrganizationUserStatusType | ProviderUserStatusType;
const MaxCheckedCount = 500;
@Directive()
export abstract class BasePeopleComponent<
UserType extends ProviderUserUserDetailsResponse | OrganizationUserView,
> {
@ViewChild("confirmTemplate", { read: ViewContainerRef, static: true })
confirmModalRef: ViewContainerRef;
get allCount() {
return this.activeUsers != null ? this.activeUsers.length : 0;
}
get invitedCount() {
return this.statusMap.has(this.userStatusType.Invited)
? this.statusMap.get(this.userStatusType.Invited).length
: 0;
}
get acceptedCount() {
return this.statusMap.has(this.userStatusType.Accepted)
? this.statusMap.get(this.userStatusType.Accepted).length
: 0;
}
get confirmedCount() {
return this.statusMap.has(this.userStatusType.Confirmed)
? this.statusMap.get(this.userStatusType.Confirmed).length
: 0;
}
get revokedCount() {
return this.statusMap.has(this.userStatusType.Revoked)
? this.statusMap.get(this.userStatusType.Revoked).length
: 0;
}
get showConfirmUsers(): boolean {
return (
this.activeUsers != null &&
this.statusMap != null &&
this.activeUsers.length > 1 &&
this.confirmedCount > 0 &&
this.confirmedCount < 3 &&
this.acceptedCount > 0
);
}
get showBulkConfirmUsers(): boolean {
return this.acceptedCount > 0;
}
abstract userType: typeof OrganizationUserType | typeof ProviderUserType;
abstract userStatusType: typeof OrganizationUserStatusType | typeof ProviderUserStatusType;
loading = true;
statusMap = new Map<StatusType, UserType[]>();
status: StatusType;
users: UserType[] = [];
pagedUsers: UserType[] = [];
actionPromise: Promise<void>;
protected allUsers: UserType[] = [];
protected activeUsers: UserType[] = [];
protected didScroll = false;
protected pageSize = 100;
protected searchControl = new FormControl("", { nonNullable: true });
protected isSearching$ = this.searchControl.valueChanges.pipe(
debounceTime(500),
concatMap((searchText) => this.searchService.isSearchable(searchText)),
startWith(false),
);
protected isPaging$ = this.isSearching$.pipe(
map((isSearching) => {
if (isSearching && this.didScroll) {
this.resetPaging();
}
return !isSearching && this.users && this.users.length > this.pageSize;
}),
);
private pagedUsersCount = 0;
constructor(
protected apiService: ApiService,
private searchService: SearchService,
protected i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService,
protected keyService: KeyService,
protected validationService: ValidationService,
private logService: LogService,
private searchPipe: SearchPipe,
protected userNamePipe: UserNamePipe,
protected dialogService: DialogService,
protected organizationManagementPreferencesService: OrganizationManagementPreferencesService,
protected toastService: ToastService,
) {}
abstract edit(user: UserType): void;
abstract getUsers(): Promise<ListResponse<UserType> | UserType[]>;
abstract deleteUser(id: string): Promise<void>;
abstract revokeUser(id: string): Promise<void>;
abstract restoreUser(id: string): Promise<void>;
abstract reinviteUser(id: string): Promise<void>;
abstract confirmUser(user: UserType, publicKey: Uint8Array): Promise<void>;
async load() {
const response = await this.getUsers();
this.statusMap.clear();
this.activeUsers = [];
for (const status of Utils.iterateEnum(this.userStatusType)) {
this.statusMap.set(status, []);
}
if (response instanceof ListResponse) {
this.allUsers = response.data != null && response.data.length > 0 ? response.data : [];
} else if (Array.isArray(response)) {
this.allUsers = response;
}
this.allUsers.sort(
Utils.getSortFunction<ProviderUserUserDetailsResponse | OrganizationUserView>(
this.i18nService,
"email",
),
);
this.allUsers.forEach((u) => {
if (!this.statusMap.has(u.status)) {
this.statusMap.set(u.status, [u]);
} else {
this.statusMap.get(u.status).push(u);
}
if (u.status !== this.userStatusType.Revoked) {
this.activeUsers.push(u);
}
});
this.filter(this.status);
this.loading = false;
}
filter(status: StatusType) {
this.status = status;
if (this.status != null) {
this.users = this.statusMap.get(this.status);
} else {
this.users = this.activeUsers;
}
// Reset checkbox selecton
this.selectAll(false);
this.resetPaging();
}
loadMore() {
if (!this.users || this.users.length <= this.pageSize) {
return;
}
const pagedLength = this.pagedUsers.length;
let pagedSize = this.pageSize;
if (pagedLength === 0 && this.pagedUsersCount > this.pageSize) {
pagedSize = this.pagedUsersCount;
}
if (this.users.length > pagedLength) {
this.pagedUsers = this.pagedUsers.concat(
this.users.slice(pagedLength, pagedLength + pagedSize),
);
}
this.pagedUsersCount = this.pagedUsers.length;
this.didScroll = this.pagedUsers.length > this.pageSize;
}
checkUser(user: UserType, select?: boolean) {
(user as any).checked = select == null ? !(user as any).checked : select;
}
selectAll(select: boolean) {
if (select) {
this.selectAll(false);
}
const filteredUsers = this.searchPipe.transform(
this.users,
this.searchControl.value,
"name",
"email",
"id",
);
const selectCount =
select && filteredUsers.length > MaxCheckedCount ? MaxCheckedCount : filteredUsers.length;
for (let i = 0; i < selectCount; i++) {
this.checkUser(filteredUsers[i], select);
}
}
resetPaging() {
this.pagedUsers = [];
this.loadMore();
}
invite() {
this.edit(null);
}
protected async removeUserConfirmationDialog(user: UserType) {
return this.dialogService.openSimpleDialog({
title: this.userNamePipe.transform(user),
content: { key: "removeUserConfirmation" },
type: "warning",
});
}
async remove(user: UserType) {
const confirmed = await this.removeUserConfirmationDialog(user);
if (!confirmed) {
return false;
}
this.actionPromise = this.deleteUser(user.id);
try {
await this.actionPromise;
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("removedUserId", this.userNamePipe.transform(user)),
});
this.removeUser(user);
} catch (e) {
this.validationService.showError(e);
}
this.actionPromise = null;
}
protected async revokeUserConfirmationDialog(user: UserType) {
return this.dialogService.openSimpleDialog({
title: { key: "revokeAccess", placeholders: [this.userNamePipe.transform(user)] },
content: this.revokeWarningMessage(),
acceptButtonText: { key: "revokeAccess" },
type: "warning",
});
}
async revoke(user: UserType) {
const confirmed = await this.revokeUserConfirmationDialog(user);
if (!confirmed) {
return false;
}
this.actionPromise = this.revokeUser(user.id);
try {
await this.actionPromise;
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("revokedUserId", this.userNamePipe.transform(user)),
});
await this.load();
} catch (e) {
this.validationService.showError(e);
}
this.actionPromise = null;
}
async restore(user: UserType) {
this.actionPromise = this.restoreUser(user.id);
try {
await this.actionPromise;
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("restoredUserId", this.userNamePipe.transform(user)),
});
await this.load();
} catch (e) {
this.validationService.showError(e);
}
this.actionPromise = null;
}
async reinvite(user: UserType) {
if (this.actionPromise != null) {
return;
}
this.actionPromise = this.reinviteUser(user.id);
try {
await this.actionPromise;
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("hasBeenReinvited", this.userNamePipe.transform(user)),
});
} catch (e) {
this.validationService.showError(e);
}
this.actionPromise = null;
}
async confirm(user: UserType) {
function updateUser(self: BasePeopleComponent<UserType>) {
user.status = self.userStatusType.Confirmed;
const mapIndex = self.statusMap.get(self.userStatusType.Accepted).indexOf(user);
if (mapIndex > -1) {
self.statusMap.get(self.userStatusType.Accepted).splice(mapIndex, 1);
self.statusMap.get(self.userStatusType.Confirmed).push(user);
}
}
const confirmUser = async (publicKey: Uint8Array) => {
try {
this.actionPromise = this.confirmUser(user, publicKey);
await this.actionPromise;
updateUser(this);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(user)),
});
} catch (e) {
this.validationService.showError(e);
throw e;
} finally {
this.actionPromise = null;
}
};
if (this.actionPromise != null) {
return;
}
try {
const publicKeyResponse = await this.apiService.getUserPublicKey(user.userId);
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
const autoConfirm = await firstValueFrom(
this.organizationManagementPreferencesService.autoConfirmFingerPrints.state$,
);
if (autoConfirm == null || !autoConfirm) {
const dialogRef = UserConfirmComponent.open(this.dialogService, {
data: {
name: this.userNamePipe.transform(user),
userId: user != null ? user.userId : null,
publicKey: publicKey,
confirmUser: () => confirmUser(publicKey),
},
});
await lastValueFrom(dialogRef.closed);
return;
}
try {
const fingerprint = await this.keyService.getFingerprint(user.userId, publicKey);
this.logService.info(`User's fingerprint: ${fingerprint.join("-")}`);
} catch (e) {
this.logService.error(e);
}
await confirmUser(publicKey);
} catch (e) {
this.logService.error(`Handled exception: ${e}`);
}
}
protected revokeWarningMessage(): string {
return this.i18nService.t("revokeUserConfirmation");
}
protected getCheckedUsers() {
return this.users.filter((u) => (u as any).checked);
}
protected removeUser(user: UserType) {
let index = this.users.indexOf(user);
if (index > -1) {
this.users.splice(index, 1);
this.resetPaging();
}
index = this.allUsers.indexOf(user);
if (index > -1) {
this.allUsers.splice(index, 1);
}
if (this.statusMap.has(user.status)) {
index = this.statusMap.get(user.status).indexOf(user);
if (index > -1) {
this.statusMap.get(user.status).splice(index, 1);
}
}
}
}

View File

@@ -178,6 +178,7 @@ export class VaultComponent implements OnInit, OnDestroy {
protected freeTrial$: Observable<FreeTrial>;
protected resellerWarning$: Observable<ResellerWarning | null>;
protected prevCipherId: string | null = null;
protected userId: UserId;
/**
* A list of collections that the user can assign items to and edit those items within.
* @protected
@@ -258,6 +259,8 @@ export class VaultComponent implements OnInit, OnDestroy {
) {}
async ngOnInit() {
this.userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
this.resellerManagedOrgAlert = await this.configService.getFeatureFlag(
FeatureFlag.ResellerManagedOrgAlert,
);
@@ -401,7 +404,7 @@ export class VaultComponent implements OnInit, OnDestroy {
ciphers = await this.cipherService.getManyFromApiForOrganization(organization.id);
}
await this.searchService.indexCiphers(ciphers, organization.id);
await this.searchService.indexCiphers(this.userId, ciphers, organization.id);
return ciphers;
}),
shareReplay({ refCount: true, bufferSize: 1 }),
@@ -445,7 +448,7 @@ export class VaultComponent implements OnInit, OnDestroy {
collectionsToReturn = selectedCollection?.children.map((c) => c.node) ?? [];
}
if (await this.searchService.isSearchable(searchText)) {
if (await this.searchService.isSearchable(this.userId, searchText)) {
collectionsToReturn = this.searchPipe.transform(
collectionsToReturn,
searchText,
@@ -519,8 +522,13 @@ export class VaultComponent implements OnInit, OnDestroy {
const filterFunction = createFilterFunction(filter);
if (await this.searchService.isSearchable(searchText)) {
return await this.searchService.searchCiphers(searchText, [filterFunction], ciphers);
if (await this.searchService.isSearchable(this.userId, searchText)) {
return await this.searchService.searchCiphers(
this.userId,
searchText,
[filterFunction],
ciphers,
);
}
return ciphers.filter(filterFunction);

View File

@@ -309,7 +309,7 @@ export class AppComponent implements OnDestroy, OnInit {
await this.stateEventRunnerService.handleEvent("logout", userId);
await this.searchService.clearIndex();
await this.searchService.clearIndex(userId);
this.authService.logOut(async () => {
await this.stateService.clean({ userId: userId });
await this.accountService.clean(userId);

View File

@@ -7,6 +7,7 @@ import { lastValueFrom } from "rxjs";
import { SendComponent as BaseSendComponent } from "@bitwarden/angular/tools/send/send.component";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -74,6 +75,7 @@ export class SendComponent extends BaseSendComponent implements OnInit, OnDestro
dialogService: DialogService,
toastService: ToastService,
private addEditFormConfigService: DefaultSendFormConfigService,
accountService: AccountService,
) {
super(
sendService,
@@ -87,6 +89,7 @@ export class SendComponent extends BaseSendComponent implements OnInit, OnDestro
sendApiService,
dialogService,
toastService,
accountService,
);
}

View File

@@ -348,8 +348,13 @@ export class VaultComponent implements OnInit, OnDestroy {
// Append any failed to decrypt ciphers to the top of the cipher list
const allCiphers = [...failedCiphers, ...ciphers];
if (await this.searchService.isSearchable(searchText)) {
return await this.searchService.searchCiphers(searchText, [filterFunction], allCiphers);
if (await this.searchService.isSearchable(activeUserId, searchText)) {
return await this.searchService.searchCiphers(
activeUserId,
searchText,
[filterFunction],
allCiphers,
);
}
return allCiphers.filter(filterFunction);
@@ -378,7 +383,7 @@ export class VaultComponent implements OnInit, OnDestroy {
collectionsToReturn = selectedCollection?.children.map((c) => c.node) ?? [];
}
if (await this.searchService.isSearchable(searchText)) {
if (await this.searchService.isSearchable(activeUserId, searchText)) {
collectionsToReturn = this.searchPipe.transform(
collectionsToReturn,
searchText,