mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 13:53:34 +00:00
[PM-5572] Event upload and collection state provider migration (#7863)
* event upload and collection state provider migration * cipher can be null when exporting org * Addressing pr comments. Casting UserId from calling methods * fixing userAuth observable in event collection service * Adding more documentation for the changes. * cli needed state provider and account services added * Addressing pr comments on modifying should update * No need to auth on event upload * Simplifying the takeEvents for pulling user events * Reverting shouldUpdate to previous state * Removing redundant comment * Removing account service for event upload * Modifying the shouldUpdate to evaluate the logic outside of the observable * Adding back in the auth for event upload service and adding event upload to the cli logout method * Adding the browser service factories * Updating the browser services away from get background * Removing event collect and upload services from browser services * Removing the audit service import * Adding the event collection migration and migration test * Event collection state needs to be stored on disk * removing event collection from state service and abstraction * removing event collection from the account data * Saving the migrations themselves
This commit is contained in:
@@ -727,14 +727,16 @@ export default class MainBackground {
|
|||||||
);
|
);
|
||||||
this.eventUploadService = new EventUploadService(
|
this.eventUploadService = new EventUploadService(
|
||||||
this.apiService,
|
this.apiService,
|
||||||
this.stateService,
|
this.stateProvider,
|
||||||
this.logService,
|
this.logService,
|
||||||
|
this.accountService,
|
||||||
);
|
);
|
||||||
this.eventCollectionService = new EventCollectionService(
|
this.eventCollectionService = new EventCollectionService(
|
||||||
this.cipherService,
|
this.cipherService,
|
||||||
this.stateService,
|
this.stateProvider,
|
||||||
this.organizationService,
|
this.organizationService,
|
||||||
this.eventUploadService,
|
this.eventUploadService,
|
||||||
|
this.accountService,
|
||||||
);
|
);
|
||||||
this.totpService = new TotpService(this.cryptoFunctionService, this.logService);
|
this.totpService = new TotpService(this.cryptoFunctionService, this.logService);
|
||||||
|
|
||||||
@@ -1108,7 +1110,7 @@ export default class MainBackground {
|
|||||||
async logout(expired: boolean, userId?: UserId) {
|
async logout(expired: boolean, userId?: UserId) {
|
||||||
userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||||
|
|
||||||
await this.eventUploadService.uploadEvents(userId);
|
await this.eventUploadService.uploadEvents(userId as UserId);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.syncService.setLastSync(new Date(0), userId),
|
this.syncService.setLastSync(new Date(0), userId),
|
||||||
|
|||||||
@@ -5,15 +5,14 @@ import {
|
|||||||
organizationServiceFactory,
|
organizationServiceFactory,
|
||||||
OrganizationServiceInitOptions,
|
OrganizationServiceInitOptions,
|
||||||
} from "../../admin-console/background/service-factories/organization-service.factory";
|
} from "../../admin-console/background/service-factories/organization-service.factory";
|
||||||
|
import { accountServiceFactory } from "../../auth/background/service-factories/account-service.factory";
|
||||||
import {
|
import {
|
||||||
FactoryOptions,
|
FactoryOptions,
|
||||||
CachedServices,
|
CachedServices,
|
||||||
factory,
|
factory,
|
||||||
} from "../../platform/background/service-factories/factory-options";
|
} from "../../platform/background/service-factories/factory-options";
|
||||||
import {
|
import { stateProviderFactory } from "../../platform/background/service-factories/state-provider.factory";
|
||||||
stateServiceFactory,
|
import { StateServiceInitOptions } from "../../platform/background/service-factories/state-service.factory";
|
||||||
StateServiceInitOptions,
|
|
||||||
} from "../../platform/background/service-factories/state-service.factory";
|
|
||||||
import {
|
import {
|
||||||
cipherServiceFactory,
|
cipherServiceFactory,
|
||||||
CipherServiceInitOptions,
|
CipherServiceInitOptions,
|
||||||
@@ -43,9 +42,10 @@ export function eventCollectionServiceFactory(
|
|||||||
async () =>
|
async () =>
|
||||||
new EventCollectionService(
|
new EventCollectionService(
|
||||||
await cipherServiceFactory(cache, opts),
|
await cipherServiceFactory(cache, opts),
|
||||||
await stateServiceFactory(cache, opts),
|
await stateProviderFactory(cache, opts),
|
||||||
await organizationServiceFactory(cache, opts),
|
await organizationServiceFactory(cache, opts),
|
||||||
await eventUploadServiceFactory(cache, opts),
|
await eventUploadServiceFactory(cache, opts),
|
||||||
|
await accountServiceFactory(cache, opts),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { EventUploadService as AbstractEventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
|
import { EventUploadService as AbstractEventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
|
||||||
import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service";
|
import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service";
|
||||||
|
|
||||||
|
import { accountServiceFactory } from "../../auth/background/service-factories/account-service.factory";
|
||||||
import {
|
import {
|
||||||
ApiServiceInitOptions,
|
ApiServiceInitOptions,
|
||||||
apiServiceFactory,
|
apiServiceFactory,
|
||||||
@@ -14,10 +15,8 @@ import {
|
|||||||
logServiceFactory,
|
logServiceFactory,
|
||||||
LogServiceInitOptions,
|
LogServiceInitOptions,
|
||||||
} from "../../platform/background/service-factories/log-service.factory";
|
} from "../../platform/background/service-factories/log-service.factory";
|
||||||
import {
|
import { stateProviderFactory } from "../../platform/background/service-factories/state-provider.factory";
|
||||||
stateServiceFactory,
|
import { StateServiceInitOptions } from "../../platform/background/service-factories/state-service.factory";
|
||||||
StateServiceInitOptions,
|
|
||||||
} from "../../platform/background/service-factories/state-service.factory";
|
|
||||||
|
|
||||||
type EventUploadServiceOptions = FactoryOptions;
|
type EventUploadServiceOptions = FactoryOptions;
|
||||||
|
|
||||||
@@ -37,8 +36,9 @@ export function eventUploadServiceFactory(
|
|||||||
async () =>
|
async () =>
|
||||||
new EventUploadService(
|
new EventUploadService(
|
||||||
await apiServiceFactory(cache, opts),
|
await apiServiceFactory(cache, opts),
|
||||||
await stateServiceFactory(cache, opts),
|
await stateProviderFactory(cache, opts),
|
||||||
await logServiceFactory(cache, opts),
|
await logServiceFactory(cache, opts),
|
||||||
|
await accountServiceFactory(cache, opts),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ import {
|
|||||||
LoginStrategyServiceAbstraction,
|
LoginStrategyServiceAbstraction,
|
||||||
} from "@bitwarden/auth/common";
|
} from "@bitwarden/auth/common";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
|
||||||
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
|
|
||||||
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
||||||
import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service";
|
import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service";
|
||||||
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
|
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
|
||||||
@@ -263,16 +261,6 @@ function getBgService<T>(service: keyof MainBackground) {
|
|||||||
useFactory: getBgService<DevicesServiceAbstraction>("devicesService"),
|
useFactory: getBgService<DevicesServiceAbstraction>("devicesService"),
|
||||||
deps: [],
|
deps: [],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
provide: EventUploadService,
|
|
||||||
useFactory: getBgService<EventUploadService>("eventUploadService"),
|
|
||||||
deps: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: EventCollectionService,
|
|
||||||
useFactory: getBgService<EventCollectionService>("eventCollectionService"),
|
|
||||||
deps: [],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
provide: PlatformUtilsService,
|
provide: PlatformUtilsService,
|
||||||
useExisting: ForegroundPlatformUtilsService,
|
useExisting: ForegroundPlatformUtilsService,
|
||||||
|
|||||||
@@ -641,15 +641,17 @@ export class Main {
|
|||||||
|
|
||||||
this.eventUploadService = new EventUploadService(
|
this.eventUploadService = new EventUploadService(
|
||||||
this.apiService,
|
this.apiService,
|
||||||
this.stateService,
|
this.stateProvider,
|
||||||
this.logService,
|
this.logService,
|
||||||
|
this.accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.eventCollectionService = new EventCollectionService(
|
this.eventCollectionService = new EventCollectionService(
|
||||||
this.cipherService,
|
this.cipherService,
|
||||||
this.stateService,
|
this.stateProvider,
|
||||||
this.organizationService,
|
this.organizationService,
|
||||||
this.eventUploadService,
|
this.eventUploadService,
|
||||||
|
this.accountService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -673,6 +675,7 @@ export class Main {
|
|||||||
});
|
});
|
||||||
const userId = await this.stateService.getUserId();
|
const userId = await this.stateService.getUserId();
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
this.eventUploadService.uploadEvents(userId as UserId),
|
||||||
this.syncService.setLastSync(new Date(0)),
|
this.syncService.setLastSync(new Date(0)),
|
||||||
this.cryptoService.clearKeys(),
|
this.cryptoService.clearKeys(),
|
||||||
this.cipherService.clear(userId),
|
this.cipherService.clear(userId),
|
||||||
|
|||||||
@@ -576,7 +576,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
let preLogoutActiveUserId;
|
let preLogoutActiveUserId;
|
||||||
try {
|
try {
|
||||||
await this.eventUploadService.uploadEvents(userBeingLoggedOut);
|
// Provide the userId of the user to upload events for
|
||||||
|
await this.eventUploadService.uploadEvents(userBeingLoggedOut as UserId);
|
||||||
await this.syncService.setLastSync(new Date(0), userBeingLoggedOut);
|
await this.syncService.setLastSync(new Date(0), userBeingLoggedOut);
|
||||||
await this.cryptoService.clearKeys(userBeingLoggedOut);
|
await this.cryptoService.clearKeys(userBeingLoggedOut);
|
||||||
await this.cipherService.clear(userBeingLoggedOut);
|
await this.cipherService.clear(userBeingLoggedOut);
|
||||||
|
|||||||
@@ -721,16 +721,17 @@ const typesafeProviders: Array<SafeProvider> = [
|
|||||||
safeProvider({
|
safeProvider({
|
||||||
provide: EventUploadServiceAbstraction,
|
provide: EventUploadServiceAbstraction,
|
||||||
useClass: EventUploadService,
|
useClass: EventUploadService,
|
||||||
deps: [ApiServiceAbstraction, StateServiceAbstraction, LogService],
|
deps: [ApiServiceAbstraction, StateProvider, LogService, AccountServiceAbstraction],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: EventCollectionServiceAbstraction,
|
provide: EventCollectionServiceAbstraction,
|
||||||
useClass: EventCollectionService,
|
useClass: EventCollectionService,
|
||||||
deps: [
|
deps: [
|
||||||
CipherServiceAbstraction,
|
CipherServiceAbstraction,
|
||||||
StateServiceAbstraction,
|
StateProvider,
|
||||||
OrganizationServiceAbstraction,
|
OrganizationServiceAbstraction,
|
||||||
EventUploadServiceAbstraction,
|
EventUploadServiceAbstraction,
|
||||||
|
AccountServiceAbstraction,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { UserId } from "../../types/guid";
|
||||||
|
|
||||||
export abstract class EventUploadService {
|
export abstract class EventUploadService {
|
||||||
uploadEvents: (userId?: string) => Promise<void>;
|
uploadEvents: (userId?: UserId) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { EventType } from "../../enums";
|
import { EventType } from "../../enums";
|
||||||
|
|
||||||
export class EventData {
|
export class EventData {
|
||||||
@@ -5,4 +7,8 @@ export class EventData {
|
|||||||
cipherId: string;
|
cipherId: string;
|
||||||
date: string;
|
date: string;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
|
|
||||||
|
static fromJSON(obj: Jsonify<EventData>): EventData {
|
||||||
|
return Object.assign(new EventData(), obj);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-re
|
|||||||
import { ForceSetPasswordReason } from "../../auth/models/domain/force-set-password-reason";
|
import { ForceSetPasswordReason } from "../../auth/models/domain/force-set-password-reason";
|
||||||
import { KdfConfig } from "../../auth/models/domain/kdf-config";
|
import { KdfConfig } from "../../auth/models/domain/kdf-config";
|
||||||
import { BiometricKey } from "../../auth/types/biometric-key";
|
import { BiometricKey } from "../../auth/types/biometric-key";
|
||||||
import { EventData } from "../../models/data/event.data";
|
|
||||||
import { WindowState } from "../../models/domain/window-state";
|
import { WindowState } from "../../models/domain/window-state";
|
||||||
import { GeneratorOptions } from "../../tools/generator/generator-options";
|
import { GeneratorOptions } from "../../tools/generator/generator-options";
|
||||||
import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password";
|
import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password";
|
||||||
@@ -253,8 +252,6 @@ export abstract class StateService<T extends Account = Account> {
|
|||||||
* @deprecated Do not call this directly, use SendService
|
* @deprecated Do not call this directly, use SendService
|
||||||
*/
|
*/
|
||||||
setEncryptedSends: (value: { [id: string]: SendData }, options?: StorageOptions) => Promise<void>;
|
setEncryptedSends: (value: { [id: string]: SendData }, options?: StorageOptions) => Promise<void>;
|
||||||
getEventCollection: (options?: StorageOptions) => Promise<EventData[]>;
|
|
||||||
setEventCollection: (value: EventData[], options?: StorageOptions) => Promise<void>;
|
|
||||||
getEverBeenUnlocked: (options?: StorageOptions) => Promise<boolean>;
|
getEverBeenUnlocked: (options?: StorageOptions) => Promise<boolean>;
|
||||||
setEverBeenUnlocked: (value: boolean, options?: StorageOptions) => Promise<void>;
|
setEverBeenUnlocked: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||||
getForceSetPasswordReason: (options?: StorageOptions) => Promise<ForceSetPasswordReason>;
|
getForceSetPasswordReason: (options?: StorageOptions) => Promise<ForceSetPasswordReason>;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-pa
|
|||||||
import { KeyConnectorUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/key-connector-user-decryption-option";
|
import { KeyConnectorUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/key-connector-user-decryption-option";
|
||||||
import { TrustedDeviceUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
|
import { TrustedDeviceUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
|
||||||
import { IdentityTokenResponse } from "../../../auth/models/response/identity-token.response";
|
import { IdentityTokenResponse } from "../../../auth/models/response/identity-token.response";
|
||||||
import { EventData } from "../../../models/data/event.data";
|
|
||||||
import { UriMatchStrategySetting } from "../../../models/domain/domain-service";
|
import { UriMatchStrategySetting } from "../../../models/domain/domain-service";
|
||||||
import { GeneratorOptions } from "../../../tools/generator/generator-options";
|
import { GeneratorOptions } from "../../../tools/generator/generator-options";
|
||||||
import {
|
import {
|
||||||
@@ -89,7 +88,6 @@ export class AccountData {
|
|||||||
GeneratedPasswordHistory[]
|
GeneratedPasswordHistory[]
|
||||||
> = new EncryptionPair<GeneratedPasswordHistory[], GeneratedPasswordHistory[]>();
|
> = new EncryptionPair<GeneratedPasswordHistory[], GeneratedPasswordHistory[]>();
|
||||||
addEditCipherInfo?: AddEditCipherInfo;
|
addEditCipherInfo?: AddEditCipherInfo;
|
||||||
eventCollection?: EventData[];
|
|
||||||
|
|
||||||
static fromJSON(obj: DeepJsonify<AccountData>): AccountData {
|
static fromJSON(obj: DeepJsonify<AccountData>): AccountData {
|
||||||
if (obj == null) {
|
if (obj == null) {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-re
|
|||||||
import { ForceSetPasswordReason } from "../../auth/models/domain/force-set-password-reason";
|
import { ForceSetPasswordReason } from "../../auth/models/domain/force-set-password-reason";
|
||||||
import { KdfConfig } from "../../auth/models/domain/kdf-config";
|
import { KdfConfig } from "../../auth/models/domain/kdf-config";
|
||||||
import { BiometricKey } from "../../auth/types/biometric-key";
|
import { BiometricKey } from "../../auth/types/biometric-key";
|
||||||
import { EventData } from "../../models/data/event.data";
|
|
||||||
import { WindowState } from "../../models/domain/window-state";
|
import { WindowState } from "../../models/domain/window-state";
|
||||||
import { GeneratorOptions } from "../../tools/generator/generator-options";
|
import { GeneratorOptions } from "../../tools/generator/generator-options";
|
||||||
import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password";
|
import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password";
|
||||||
@@ -1176,24 +1175,6 @@ export class StateService<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@withPrototypeForArrayMembers(EventData)
|
|
||||||
async getEventCollection(options?: StorageOptions): Promise<EventData[]> {
|
|
||||||
return (
|
|
||||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
|
||||||
)?.data?.eventCollection;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setEventCollection(value: EventData[], options?: StorageOptions): Promise<void> {
|
|
||||||
const account = await this.getAccount(
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
account.data.eventCollection = value;
|
|
||||||
await this.saveAccount(
|
|
||||||
account,
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getEverBeenUnlocked(options?: StorageOptions): Promise<boolean> {
|
async getEverBeenUnlocked(options?: StorageOptions): Promise<boolean> {
|
||||||
return (
|
return (
|
||||||
(await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())))
|
(await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())))
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ export const SM_ONBOARDING_DISK = new StateDefinition("smOnboarding", "disk", {
|
|||||||
|
|
||||||
export const GENERATOR_DISK = new StateDefinition("generator", "disk");
|
export const GENERATOR_DISK = new StateDefinition("generator", "disk");
|
||||||
export const GENERATOR_MEMORY = new StateDefinition("generator", "memory");
|
export const GENERATOR_MEMORY = new StateDefinition("generator", "memory");
|
||||||
|
export const EVENT_COLLECTION_DISK = new StateDefinition("eventCollection", "disk");
|
||||||
|
|
||||||
// Vault
|
// Vault
|
||||||
|
|
||||||
|
|||||||
@@ -1,61 +1,105 @@
|
|||||||
|
import { firstValueFrom, map, from, zip } from "rxjs";
|
||||||
|
|
||||||
import { EventCollectionService as EventCollectionServiceAbstraction } from "../../abstractions/event/event-collection.service";
|
import { EventCollectionService as EventCollectionServiceAbstraction } from "../../abstractions/event/event-collection.service";
|
||||||
import { EventUploadService } from "../../abstractions/event/event-upload.service";
|
import { EventUploadService } from "../../abstractions/event/event-upload.service";
|
||||||
import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { AccountService } from "../../auth/abstractions/account.service";
|
||||||
|
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
|
||||||
import { EventType } from "../../enums";
|
import { EventType } from "../../enums";
|
||||||
import { EventData } from "../../models/data/event.data";
|
import { EventData } from "../../models/data/event.data";
|
||||||
import { StateService } from "../../platform/abstractions/state.service";
|
import { StateProvider } from "../../platform/state";
|
||||||
import { CipherService } from "../../vault/abstractions/cipher.service";
|
import { CipherService } from "../../vault/abstractions/cipher.service";
|
||||||
|
|
||||||
|
import { EVENT_COLLECTION } from "./key-definitions";
|
||||||
|
|
||||||
export class EventCollectionService implements EventCollectionServiceAbstraction {
|
export class EventCollectionService implements EventCollectionServiceAbstraction {
|
||||||
constructor(
|
constructor(
|
||||||
private cipherService: CipherService,
|
private cipherService: CipherService,
|
||||||
private stateService: StateService,
|
private stateProvider: StateProvider,
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
private eventUploadService: EventUploadService,
|
private eventUploadService: EventUploadService,
|
||||||
|
private accountService: AccountService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/** Adds an event to the active user's event collection
|
||||||
|
* @param eventType the event type to be added
|
||||||
|
* @param cipherId if provided the id of the cipher involved in the event
|
||||||
|
* @param uploadImmediately in some cases the recorded events should be uploaded right after being added
|
||||||
|
* @param organizationId the organizationId involved in the event. If the cipherId is not provided an organizationId is required
|
||||||
|
*/
|
||||||
async collect(
|
async collect(
|
||||||
eventType: EventType,
|
eventType: EventType,
|
||||||
cipherId: string = null,
|
cipherId: string = null,
|
||||||
uploadImmediately = false,
|
uploadImmediately = false,
|
||||||
organizationId: string = null,
|
organizationId: string = null,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const authed = await this.stateService.getIsAuthenticated();
|
const userId = await firstValueFrom(this.stateProvider.activeUserId$);
|
||||||
if (!authed) {
|
const eventStore = this.stateProvider.getUser(userId, EVENT_COLLECTION);
|
||||||
|
|
||||||
|
if (!(await this.shouldUpdate(cipherId, organizationId))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const organizations = await this.organizationService.getAll();
|
|
||||||
if (organizations == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const orgIds = new Set<string>(organizations.filter((o) => o.useEvents).map((o) => o.id));
|
|
||||||
if (orgIds.size === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (cipherId != null) {
|
|
||||||
const cipher = await this.cipherService.get(cipherId);
|
|
||||||
if (cipher == null || cipher.organizationId == null || !orgIds.has(cipher.organizationId)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (organizationId != null) {
|
|
||||||
if (!orgIds.has(organizationId)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let eventCollection = await this.stateService.getEventCollection();
|
|
||||||
if (eventCollection == null) {
|
|
||||||
eventCollection = [];
|
|
||||||
}
|
|
||||||
const event = new EventData();
|
const event = new EventData();
|
||||||
event.type = eventType;
|
event.type = eventType;
|
||||||
event.cipherId = cipherId;
|
event.cipherId = cipherId;
|
||||||
event.date = new Date().toISOString();
|
event.date = new Date().toISOString();
|
||||||
event.organizationId = organizationId;
|
event.organizationId = organizationId;
|
||||||
eventCollection.push(event);
|
|
||||||
await this.stateService.setEventCollection(eventCollection);
|
await eventStore.update((events) => {
|
||||||
|
events = events ?? [];
|
||||||
|
events.push(event);
|
||||||
|
return events;
|
||||||
|
});
|
||||||
|
|
||||||
if (uploadImmediately) {
|
if (uploadImmediately) {
|
||||||
await this.eventUploadService.uploadEvents();
|
await this.eventUploadService.uploadEvents();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Verifies if the event collection should be updated for the provided information
|
||||||
|
* @param cipherId the cipher for the event
|
||||||
|
* @param organizationId the organization for the event
|
||||||
|
*/
|
||||||
|
private async shouldUpdate(
|
||||||
|
cipherId: string = null,
|
||||||
|
organizationId: string = null,
|
||||||
|
): Promise<boolean> {
|
||||||
|
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 [accountInfo, orgIds, cipher] = await firstValueFrom(
|
||||||
|
zip(this.accountService.activeAccount$, orgIds$, cipher$),
|
||||||
|
);
|
||||||
|
|
||||||
|
// The user must be authorized
|
||||||
|
if (accountInfo.status != AuthenticationStatus.Unlocked) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// User must have organizations assigned to them
|
||||||
|
if (orgIds == null || orgIds.length == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the cipher is null there must be an organization id provided
|
||||||
|
if (cipher == null && 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 organization id is provided it must be in the user's org list
|
||||||
|
if (organizationId != null && !orgIds.includes(organizationId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,24 @@
|
|||||||
|
import { firstValueFrom, map } from "rxjs";
|
||||||
|
|
||||||
import { ApiService } from "../../abstractions/api.service";
|
import { ApiService } from "../../abstractions/api.service";
|
||||||
import { EventUploadService as EventUploadServiceAbstraction } from "../../abstractions/event/event-upload.service";
|
import { EventUploadService as EventUploadServiceAbstraction } from "../../abstractions/event/event-upload.service";
|
||||||
|
import { AccountService } from "../../auth/abstractions/account.service";
|
||||||
|
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
|
||||||
|
import { EventData } from "../../models/data/event.data";
|
||||||
import { EventRequest } from "../../models/request/event.request";
|
import { EventRequest } from "../../models/request/event.request";
|
||||||
import { LogService } from "../../platform/abstractions/log.service";
|
import { LogService } from "../../platform/abstractions/log.service";
|
||||||
import { StateService } from "../../platform/abstractions/state.service";
|
import { StateProvider } from "../../platform/state";
|
||||||
|
import { UserId } from "../../types/guid";
|
||||||
|
|
||||||
|
import { EVENT_COLLECTION } from "./key-definitions";
|
||||||
|
|
||||||
export class EventUploadService implements EventUploadServiceAbstraction {
|
export class EventUploadService implements EventUploadServiceAbstraction {
|
||||||
private inited = false;
|
private inited = false;
|
||||||
constructor(
|
constructor(
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private stateService: StateService,
|
private stateProvider: StateProvider,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
|
private accountService: AccountService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
init(checkOnInterval: boolean) {
|
init(checkOnInterval: boolean) {
|
||||||
@@ -26,12 +35,26 @@ export class EventUploadService implements EventUploadServiceAbstraction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadEvents(userId?: string): Promise<void> {
|
/** Upload the event collection from state.
|
||||||
const authed = await this.stateService.getIsAuthenticated({ userId: userId });
|
* @param userId upload events for provided user. If not active user will be used.
|
||||||
if (!authed) {
|
*/
|
||||||
|
async uploadEvents(userId?: UserId): Promise<void> {
|
||||||
|
if (!userId) {
|
||||||
|
userId = await firstValueFrom(this.stateProvider.activeUserId$);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the auth status from the provided user or the active user
|
||||||
|
const userAuth$ = this.accountService.accounts$.pipe(
|
||||||
|
map((accounts) => accounts[userId]?.status === AuthenticationStatus.Unlocked),
|
||||||
|
);
|
||||||
|
|
||||||
|
const isAuthenticated = await firstValueFrom(userAuth$);
|
||||||
|
if (!isAuthenticated) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const eventCollection = await this.stateService.getEventCollection({ userId: userId });
|
|
||||||
|
const eventCollection = await this.takeEvents(userId);
|
||||||
|
|
||||||
if (eventCollection == null || eventCollection.length === 0) {
|
if (eventCollection == null || eventCollection.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -45,15 +68,23 @@ export class EventUploadService implements EventUploadServiceAbstraction {
|
|||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await this.apiService.postEventsCollect(request);
|
await this.apiService.postEventsCollect(request);
|
||||||
// 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.clearEvents(userId);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logService.error(e);
|
this.logService.error(e);
|
||||||
|
// Add the events back to state if there was an error and they were not uploaded.
|
||||||
|
await this.stateProvider.setUserState(EVENT_COLLECTION, eventCollection, userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async clearEvents(userId?: string): Promise<any> {
|
/** Return user's events and then clear them from state
|
||||||
await this.stateService.setEventCollection(null, { userId: userId });
|
* @param userId the user to grab and clear events for
|
||||||
|
*/
|
||||||
|
private async takeEvents(userId: UserId): Promise<EventData[]> {
|
||||||
|
let taken = null;
|
||||||
|
await this.stateProvider.getUser(userId, EVENT_COLLECTION).update((current) => {
|
||||||
|
taken = current ?? [];
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
return taken;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
libs/common/src/services/event/key-definitions.ts
Normal file
10
libs/common/src/services/event/key-definitions.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { EventData } from "../../models/data/event.data";
|
||||||
|
import { KeyDefinition, EVENT_COLLECTION_DISK } from "../../platform/state";
|
||||||
|
|
||||||
|
export const EVENT_COLLECTION: KeyDefinition<EventData[]> = KeyDefinition.array<EventData>(
|
||||||
|
EVENT_COLLECTION_DISK,
|
||||||
|
"events",
|
||||||
|
{
|
||||||
|
deserializer: (s) => EventData.fromJSON(s),
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -36,6 +36,7 @@ import { TokenServiceStateProviderMigrator } from "./migrations/38-migrate-token
|
|||||||
import { MoveBillingAccountProfileMigrator } from "./migrations/39-move-billing-account-profile-to-state-providers";
|
import { MoveBillingAccountProfileMigrator } from "./migrations/39-move-billing-account-profile-to-state-providers";
|
||||||
import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been-unlocked";
|
import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been-unlocked";
|
||||||
import { OrganizationMigrator } from "./migrations/40-move-organization-state-to-state-provider";
|
import { OrganizationMigrator } from "./migrations/40-move-organization-state-to-state-provider";
|
||||||
|
import { EventCollectionMigrator } from "./migrations/41-move-event-collection-to-state-provider";
|
||||||
import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys";
|
import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys";
|
||||||
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
|
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
|
||||||
import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
|
import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
|
||||||
@@ -44,7 +45,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting
|
|||||||
import { MinVersionMigrator } from "./migrations/min-version";
|
import { MinVersionMigrator } from "./migrations/min-version";
|
||||||
|
|
||||||
export const MIN_VERSION = 3;
|
export const MIN_VERSION = 3;
|
||||||
export const CURRENT_VERSION = 40;
|
export const CURRENT_VERSION = 41;
|
||||||
export type MinVersion = typeof MIN_VERSION;
|
export type MinVersion = typeof MIN_VERSION;
|
||||||
|
|
||||||
export function createMigrationBuilder() {
|
export function createMigrationBuilder() {
|
||||||
@@ -86,7 +87,8 @@ export function createMigrationBuilder() {
|
|||||||
.with(AvatarColorMigrator, 36, 37)
|
.with(AvatarColorMigrator, 36, 37)
|
||||||
.with(TokenServiceStateProviderMigrator, 37, 38)
|
.with(TokenServiceStateProviderMigrator, 37, 38)
|
||||||
.with(MoveBillingAccountProfileMigrator, 38, 39)
|
.with(MoveBillingAccountProfileMigrator, 38, 39)
|
||||||
.with(OrganizationMigrator, 39, CURRENT_VERSION);
|
.with(OrganizationMigrator, 39, 40)
|
||||||
|
.with(EventCollectionMigrator, 40, CURRENT_VERSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function currentVersion(
|
export async function currentVersion(
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
import { MockProxy, any } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { MigrationHelper } from "../migration-helper";
|
||||||
|
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||||
|
|
||||||
|
import { EventCollectionMigrator } from "./41-move-event-collection-to-state-provider";
|
||||||
|
|
||||||
|
function exampleJSON() {
|
||||||
|
return {
|
||||||
|
global: {
|
||||||
|
otherStuff: "otherStuff1",
|
||||||
|
},
|
||||||
|
authenticatedAccounts: ["user-1", "user-2"],
|
||||||
|
"user-1": {
|
||||||
|
data: {
|
||||||
|
eventCollection: [
|
||||||
|
{
|
||||||
|
type: 1107,
|
||||||
|
cipherId: "5154f91d-c469-4d23-aefa-b12a0140d684",
|
||||||
|
organizationId: "278d5f91-835b-459a-a229-b11e01336d6d",
|
||||||
|
date: "2024-03-05T21:59:50.169Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 1107,
|
||||||
|
cipherId: "ed4661bd-412c-4b05-89a2-b12a01697a2c",
|
||||||
|
organizationId: "278d5f91-835b-459a-a229-b11e01336d6d",
|
||||||
|
date: "2024-03-05T22:02:06.089Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
otherStuff: "otherStuff2",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff3",
|
||||||
|
},
|
||||||
|
"user-2": {
|
||||||
|
data: {
|
||||||
|
otherStuff: "otherStuff4",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff5",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function rollbackJSON() {
|
||||||
|
return {
|
||||||
|
"user_user-1_eventCollection_eventCollection": [
|
||||||
|
{
|
||||||
|
type: 1107,
|
||||||
|
cipherId: "5154f91d-c469-4d23-aefa-b12a0140d684",
|
||||||
|
organizationId: "278d5f91-835b-459a-a229-b11e01336d6d",
|
||||||
|
date: "2024-03-05T21:59:50.169Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 1107,
|
||||||
|
cipherId: "ed4661bd-412c-4b05-89a2-b12a01697a2c",
|
||||||
|
organizationId: "278d5f91-835b-459a-a229-b11e01336d6d",
|
||||||
|
date: "2024-03-05T22:02:06.089Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"user_user-2_eventCollection_data": null as any,
|
||||||
|
global: {
|
||||||
|
otherStuff: "otherStuff1",
|
||||||
|
},
|
||||||
|
authenticatedAccounts: ["user-1", "user-2"],
|
||||||
|
"user-1": {
|
||||||
|
data: {
|
||||||
|
otherStuff: "otherStuff2",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff3",
|
||||||
|
},
|
||||||
|
"user-2": {
|
||||||
|
data: {
|
||||||
|
otherStuff: "otherStuff4",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff5",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("EventCollectionMigrator", () => {
|
||||||
|
let helper: MockProxy<MigrationHelper>;
|
||||||
|
let sut: EventCollectionMigrator;
|
||||||
|
const keyDefinitionLike = {
|
||||||
|
stateDefinition: {
|
||||||
|
name: "eventCollection",
|
||||||
|
},
|
||||||
|
key: "eventCollection",
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("migrate", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
helper = mockMigrationHelper(exampleJSON(), 40);
|
||||||
|
sut = new EventCollectionMigrator(40, 41);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove event collections from all accounts", async () => {
|
||||||
|
await sut.migrate(helper);
|
||||||
|
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||||
|
data: {
|
||||||
|
otherStuff: "otherStuff2",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff3",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set event collections for each account", async () => {
|
||||||
|
await sut.migrate(helper);
|
||||||
|
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith("user-1", keyDefinitionLike, [
|
||||||
|
{
|
||||||
|
type: 1107,
|
||||||
|
cipherId: "5154f91d-c469-4d23-aefa-b12a0140d684",
|
||||||
|
organizationId: "278d5f91-835b-459a-a229-b11e01336d6d",
|
||||||
|
date: "2024-03-05T21:59:50.169Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 1107,
|
||||||
|
cipherId: "ed4661bd-412c-4b05-89a2-b12a01697a2c",
|
||||||
|
organizationId: "278d5f91-835b-459a-a229-b11e01336d6d",
|
||||||
|
date: "2024-03-05T22:02:06.089Z",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("rollback", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
helper = mockMigrationHelper(rollbackJSON(), 41);
|
||||||
|
sut = new EventCollectionMigrator(40, 41);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(["user-1", "user-2"])("should null out new values", async (userId) => {
|
||||||
|
await sut.rollback(helper);
|
||||||
|
expect(helper.setToUser).toHaveBeenCalledWith(userId, keyDefinitionLike, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add event collection values back to accounts", async () => {
|
||||||
|
await sut.rollback(helper);
|
||||||
|
|
||||||
|
expect(helper.set).toHaveBeenCalled();
|
||||||
|
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||||
|
data: {
|
||||||
|
eventCollection: [
|
||||||
|
{
|
||||||
|
type: 1107,
|
||||||
|
cipherId: "5154f91d-c469-4d23-aefa-b12a0140d684",
|
||||||
|
organizationId: "278d5f91-835b-459a-a229-b11e01336d6d",
|
||||||
|
date: "2024-03-05T21:59:50.169Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 1107,
|
||||||
|
cipherId: "ed4661bd-412c-4b05-89a2-b12a01697a2c",
|
||||||
|
organizationId: "278d5f91-835b-459a-a229-b11e01336d6d",
|
||||||
|
date: "2024-03-05T22:02:06.089Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
otherStuff: "otherStuff2",
|
||||||
|
},
|
||||||
|
otherStuff: "otherStuff3",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not try to restore values to missing accounts", async () => {
|
||||||
|
await sut.rollback(helper);
|
||||||
|
|
||||||
|
expect(helper.set).not.toHaveBeenCalledWith("user-3", any());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||||
|
import { Migrator } from "../migrator";
|
||||||
|
|
||||||
|
type ExpectedAccountState = {
|
||||||
|
data?: {
|
||||||
|
eventCollection?: [];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const EVENT_COLLECTION: KeyDefinitionLike = {
|
||||||
|
stateDefinition: {
|
||||||
|
name: "eventCollection",
|
||||||
|
},
|
||||||
|
key: "eventCollection",
|
||||||
|
};
|
||||||
|
|
||||||
|
export class EventCollectionMigrator extends Migrator<40, 41> {
|
||||||
|
async migrate(helper: MigrationHelper): Promise<void> {
|
||||||
|
const accounts = await helper.getAccounts<ExpectedAccountState>();
|
||||||
|
|
||||||
|
async function migrateAccount(userId: string, account: ExpectedAccountState): Promise<void> {
|
||||||
|
const value = account?.data?.eventCollection;
|
||||||
|
if (value != null) {
|
||||||
|
await helper.setToUser(userId, EVENT_COLLECTION, value);
|
||||||
|
delete account.data.eventCollection;
|
||||||
|
await helper.set(userId, account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async rollback(helper: MigrationHelper): Promise<void> {
|
||||||
|
const accounts = await helper.getAccounts<ExpectedAccountState>();
|
||||||
|
|
||||||
|
async function rollbackAccount(userId: string, account: ExpectedAccountState): Promise<void> {
|
||||||
|
const value = await helper.getFromUser(userId, EVENT_COLLECTION);
|
||||||
|
if (account) {
|
||||||
|
account.data = Object.assign(account.data ?? {}, {
|
||||||
|
eventCollection: value,
|
||||||
|
});
|
||||||
|
|
||||||
|
await helper.set(userId, account);
|
||||||
|
}
|
||||||
|
await helper.setToUser(userId, EVENT_COLLECTION, null);
|
||||||
|
}
|
||||||
|
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user