diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 8e11e7e193..0ea0be9ffd 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -869,6 +869,7 @@ export default class MainBackground { this.organizationService, this.eventUploadService, this.authService, + this.accountService, ); this.totpService = new TotpService(this.cryptoFunctionService, this.logService); diff --git a/apps/cli/src/commands/list.command.ts b/apps/cli/src/commands/list.command.ts index 536c9e3b8c..88574635e1 100644 --- a/apps/cli/src/commands/list.command.ts +++ b/apps/cli/src/commands/list.command.ts @@ -128,17 +128,7 @@ export class ListCommand { ciphers = this.searchService.searchCiphersBasic(ciphers, options.search, options.trash); } - for (let i = 0; i < ciphers.length; i++) { - const c = ciphers[i]; - // Set upload immediately on the last item in the ciphers collection to avoid the event collection - // service from uploading each time. - await this.eventCollectionService.collect( - EventType.Cipher_ClientViewed, - c.id, - i === ciphers.length - 1, - c.organizationId, - ); - } + await this.eventCollectionService.collectMany(EventType.Cipher_ClientViewed, ciphers, true); const res = new ListResponse(ciphers.map((o) => new CipherResponse(o))); return Response.success(res); diff --git a/apps/cli/src/service-container.ts b/apps/cli/src/service-container.ts index 2d5f83787f..fe9de86822 100644 --- a/apps/cli/src/service-container.ts +++ b/apps/cli/src/service-container.ts @@ -729,6 +729,7 @@ export class ServiceContainer { this.organizationService, this.eventUploadService, this.authService, + this.accountService, ); this.providerApiService = new ProviderApiService(this.apiService); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index e06b278876..388a149bfa 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -830,6 +830,7 @@ const safeProviders: SafeProvider[] = [ OrganizationServiceAbstraction, EventUploadServiceAbstraction, AuthServiceAbstraction, + AccountServiceAbstraction, ], }), safeProvider({ diff --git a/libs/common/src/abstractions/event/event-collection.service.ts b/libs/common/src/abstractions/event/event-collection.service.ts index 15930dbc2e..38f226cc2d 100644 --- a/libs/common/src/abstractions/event/event-collection.service.ts +++ b/libs/common/src/abstractions/event/event-collection.service.ts @@ -1,6 +1,12 @@ import { EventType } from "../../enums"; +import { CipherView } from "../../vault/models/view/cipher.view"; export abstract class EventCollectionService { + collectMany: ( + eventType: EventType, + ciphers: CipherView[], + uploadImmediately?: boolean, + ) => Promise; collect: ( eventType: EventType, cipherId?: string, diff --git a/libs/common/src/services/event/event-collection.service.ts b/libs/common/src/services/event/event-collection.service.ts index 1482bb8b61..570d84c659 100644 --- a/libs/common/src/services/event/event-collection.service.ts +++ b/libs/common/src/services/event/event-collection.service.ts @@ -1,25 +1,76 @@ -import { firstValueFrom, map, from, zip } from "rxjs"; +import { firstValueFrom, map, from, zip, Observable } from "rxjs"; import { EventCollectionService as EventCollectionServiceAbstraction } from "../../abstractions/event/event-collection.service"; import { EventUploadService } from "../../abstractions/event/event-upload.service"; import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "../../auth/abstractions/account.service"; import { AuthService } from "../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { EventType } from "../../enums"; import { EventData } from "../../models/data/event.data"; import { StateProvider } from "../../platform/state"; import { CipherService } from "../../vault/abstractions/cipher.service"; +import { CipherView } from "../../vault/models/view/cipher.view"; import { EVENT_COLLECTION } from "./key-definitions"; export class EventCollectionService implements EventCollectionServiceAbstraction { + private orgIds$: Observable; + constructor( private cipherService: CipherService, private stateProvider: StateProvider, private organizationService: OrganizationService, private eventUploadService: EventUploadService, private authService: AuthService, - ) {} + private accountService: AccountService, + ) { + this.orgIds$ = this.organizationService.organizations$.pipe( + map((orgs) => orgs?.filter((o) => o.useEvents)?.map((x) => x.id) ?? []), + ); + } + + /** Adds an event to the active user's event collection + * @param eventType the event type to be added + * @param ciphers The collection of ciphers to log events for + * @param uploadImmediately in some cases the recorded events should be uploaded right after being added + */ + async collectMany( + eventType: EventType, + ciphers: CipherView[], + uploadImmediately = false, + ): Promise { + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); + const eventStore = this.stateProvider.getUser(userId, EVENT_COLLECTION); + + if (!(await this.shouldUpdate(null, eventType, ciphers))) { + return; + } + + const events$ = this.orgIds$.pipe( + map((orgs) => + ciphers + .filter((c) => orgs.includes(c.organizationId)) + .map((c) => ({ + type: eventType, + cipherId: c.id, + date: new Date().toISOString(), + organizationId: c.organizationId, + })), + ), + ); + + await eventStore.update( + (currentEvents, newEvents) => [...(currentEvents ?? []), ...newEvents], + { + combineLatestWith: events$, + }, + ); + + if (uploadImmediately) { + await this.eventUploadService.uploadEvents(); + } + } /** Adds an event to the active user's event collection * @param eventType the event type to be added @@ -33,10 +84,10 @@ export class EventCollectionService implements EventCollectionServiceAbstraction uploadImmediately = false, organizationId: string = null, ): Promise { - const userId = await firstValueFrom(this.stateProvider.activeUserId$); + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); const eventStore = this.stateProvider.getUser(userId, EVENT_COLLECTION); - if (!(await this.shouldUpdate(cipherId, organizationId, eventType))) { + if (!(await this.shouldUpdate(organizationId, eventType, undefined, cipherId))) { return; } @@ -62,18 +113,15 @@ export class EventCollectionService implements EventCollectionServiceAbstraction * @param organizationId the organization for the event */ private async shouldUpdate( - cipherId: string = null, organizationId: string = null, eventType: EventType = null, + ciphers: CipherView[] = [], + cipherId?: string, ): Promise { - const orgIds$ = this.organizationService.organizations$.pipe( - map((orgs) => orgs?.filter((o) => o.useEvents)?.map((x) => x.id) ?? []), - ); - const cipher$ = from(this.cipherService.get(cipherId)); const [authStatus, orgIds, cipher] = await firstValueFrom( - zip(this.authService.activeAccountStatus$, orgIds$, cipher$), + zip(this.authService.activeAccountStatus$, this.orgIds$, cipher$), ); // The user must be authorized @@ -91,14 +139,21 @@ export class EventCollectionService implements EventCollectionServiceAbstraction return true; } - // If the cipher is null there must be an organization id provided - if (cipher == null && organizationId == null) { + // If the cipherId was provided and a cipher exists, add it to the collection + if (cipher != null) { + ciphers.push(new CipherView(cipher)); + } + + // If no ciphers there must be an organization id provided + if ((ciphers == null || ciphers.length == 0) && organizationId == null) { return false; } - // If the cipher is present it must be in the user's org list - if (cipher != null && !orgIds.includes(cipher?.organizationId)) { - return false; + // If the input list of ciphers is provided. Check the ciphers to see if any + // are in the user's org list + if (ciphers != null && ciphers.length > 0) { + const filtered = ciphers.filter((c) => orgIds.includes(c.organizationId)); + return filtered.length > 0; } // If the organization id is provided it must be in the user's org list