diff --git a/packages/auth/src/Bootstrap/Container.ts b/packages/auth/src/Bootstrap/Container.ts index 82ff81a46..313caddca 100644 --- a/packages/auth/src/Bootstrap/Container.ts +++ b/packages/auth/src/Bootstrap/Container.ts @@ -280,6 +280,7 @@ import { TriggerEmailBackupForAllUsers } from '../Domain/UseCase/TriggerEmailBac import { CSVFileReaderInterface } from '../Domain/CSV/CSVFileReaderInterface' import { S3CsvFileReader } from '../Infra/S3/S3CsvFileReader' import { DeleteAccountsFromCSVFile } from '../Domain/UseCase/DeleteAccountsFromCSVFile/DeleteAccountsFromCSVFile' +import { AccountDeletionVerificationPassedEventHandler } from '../Domain/Handler/AccountDeletionVerificationPassedEventHandler' export class ContainerConfigLoader { constructor(private mode: 'server' | 'worker' = 'server') {} @@ -1274,7 +1275,9 @@ export class ContainerConfigLoader { .toConstantValue( new DeleteAccountsFromCSVFile( container.get(TYPES.Auth_CSVFileReader), - container.get(TYPES.Auth_DeleteAccount), + container.get(TYPES.Auth_DomainEventPublisher), + container.get(TYPES.Auth_DomainEventFactory), + container.get(TYPES.Auth_UserRepository), container.get(TYPES.Auth_Logger), ), ) @@ -1328,6 +1331,14 @@ export class ContainerConfigLoader { container.get(TYPES.Auth_Logger), ), ) + container + .bind(TYPES.Auth_AccountDeletionVerificationPassedEventHandler) + .toConstantValue( + new AccountDeletionVerificationPassedEventHandler( + container.get(TYPES.Auth_DeleteAccount), + container.get(TYPES.Auth_Logger), + ), + ) container .bind(TYPES.Auth_SubscriptionPurchasedEventHandler) .toConstantValue( @@ -1516,6 +1527,7 @@ export class ContainerConfigLoader { const eventHandlers: Map = new Map([ ['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.Auth_AccountDeletionRequestedEventHandler)], + ['ACCOUNT_DELETION_VERIFICATION_PASSED', container.get(TYPES.Auth_AccountDeletionVerificationPassedEventHandler)], ['SUBSCRIPTION_PURCHASED', container.get(TYPES.Auth_SubscriptionPurchasedEventHandler)], ['SUBSCRIPTION_CANCELLED', container.get(TYPES.Auth_SubscriptionCancelledEventHandler)], ['SUBSCRIPTION_RENEWED', container.get(TYPES.Auth_SubscriptionRenewedEventHandler)], diff --git a/packages/auth/src/Bootstrap/Types.ts b/packages/auth/src/Bootstrap/Types.ts index e01459364..f6a59b8e9 100644 --- a/packages/auth/src/Bootstrap/Types.ts +++ b/packages/auth/src/Bootstrap/Types.ts @@ -171,6 +171,7 @@ const TYPES = { Auth_DeleteAccountsFromCSVFile: Symbol.for('Auth_DeleteAccountsFromCSVFile'), // Handlers Auth_AccountDeletionRequestedEventHandler: Symbol.for('Auth_AccountDeletionRequestedEventHandler'), + Auth_AccountDeletionVerificationPassedEventHandler: Symbol.for('Auth_AccountDeletionVerificationPassedEventHandler'), Auth_SubscriptionPurchasedEventHandler: Symbol.for('Auth_SubscriptionPurchasedEventHandler'), Auth_SubscriptionCancelledEventHandler: Symbol.for('Auth_SubscriptionCancelledEventHandler'), Auth_SubscriptionReassignedEventHandler: Symbol.for('Auth_SubscriptionReassignedEventHandler'), diff --git a/packages/auth/src/Domain/Event/DomainEventFactory.ts b/packages/auth/src/Domain/Event/DomainEventFactory.ts index 5a1fa8f8f..86f7435ef 100644 --- a/packages/auth/src/Domain/Event/DomainEventFactory.ts +++ b/packages/auth/src/Domain/Event/DomainEventFactory.ts @@ -20,6 +20,7 @@ import { StatisticPersistenceRequestedEvent, SessionCreatedEvent, SessionRefreshedEvent, + AccountDeletionVerificationRequestedEvent, } from '@standardnotes/domain-events' import { Predicate, PredicateVerificationResult } from '@standardnotes/predicates' import { TimerInterface } from '@standardnotes/time' @@ -33,6 +34,24 @@ import { KeyParamsData } from '@standardnotes/responses' export class DomainEventFactory implements DomainEventFactoryInterface { constructor(@inject(TYPES.Auth_Timer) private timer: TimerInterface) {} + createAccountDeletionVerificationRequestedEvent(dto: { + userUuid: string + email: string + }): AccountDeletionVerificationRequestedEvent { + return { + type: 'ACCOUNT_DELETION_VERIFICATION_REQUESTED', + createdAt: this.timer.getUTCDate(), + meta: { + correlation: { + userIdentifier: dto.userUuid, + userIdentifierType: 'uuid', + }, + origin: DomainEventService.Auth, + }, + payload: dto, + } + } + createSessionCreatedEvent(dto: { userUuid: string }): SessionCreatedEvent { return { type: 'SESSION_CREATED', diff --git a/packages/auth/src/Domain/Event/DomainEventFactoryInterface.ts b/packages/auth/src/Domain/Event/DomainEventFactoryInterface.ts index e915bb54b..6c3fd38e8 100644 --- a/packages/auth/src/Domain/Event/DomainEventFactoryInterface.ts +++ b/packages/auth/src/Domain/Event/DomainEventFactoryInterface.ts @@ -18,6 +18,7 @@ import { StatisticPersistenceRequestedEvent, SessionCreatedEvent, SessionRefreshedEvent, + AccountDeletionVerificationRequestedEvent, } from '@standardnotes/domain-events' import { InviteeIdentifierType } from '../SharedSubscription/InviteeIdentifierType' import { KeyParamsData } from '@standardnotes/responses' @@ -56,6 +57,10 @@ export interface DomainEventFactoryInterface { ownerUuid: string } }): AccountDeletionRequestedEvent + createAccountDeletionVerificationRequestedEvent(dto: { + userUuid: string + email: string + }): AccountDeletionVerificationRequestedEvent createUserRolesChangedEvent(userUuid: string, email: string, currentRoles: string[]): UserRolesChangedEvent createUserEmailChangedEvent(userUuid: string, fromEmail: string, toEmail: string): UserEmailChangedEvent createUserDisabledSessionUserAgentLoggingEvent(dto: { diff --git a/packages/auth/src/Domain/Handler/AccountDeletionVerificationPassedEventHandler.ts b/packages/auth/src/Domain/Handler/AccountDeletionVerificationPassedEventHandler.ts new file mode 100644 index 000000000..e9ac9289f --- /dev/null +++ b/packages/auth/src/Domain/Handler/AccountDeletionVerificationPassedEventHandler.ts @@ -0,0 +1,21 @@ +import { AccountDeletionVerificationPassedEvent, DomainEventHandlerInterface } from '@standardnotes/domain-events' +import { Logger } from 'winston' + +import { DeleteAccount } from '../UseCase/DeleteAccount/DeleteAccount' + +export class AccountDeletionVerificationPassedEventHandler implements DomainEventHandlerInterface { + constructor( + private deleteAccount: DeleteAccount, + private logger: Logger, + ) {} + + async handle(event: AccountDeletionVerificationPassedEvent): Promise { + const result = await this.deleteAccount.execute({ + userUuid: event.payload.userUuid, + }) + + if (result.isFailed()) { + this.logger.error(`AccountDeletionVerificationPassedEventHandler failed: ${result.getError()}`) + } + } +} diff --git a/packages/auth/src/Domain/UseCase/DeleteAccountsFromCSVFile/DeleteAccountsFromCSVFile.spec.ts b/packages/auth/src/Domain/UseCase/DeleteAccountsFromCSVFile/DeleteAccountsFromCSVFile.spec.ts index 03b7b9d62..905a7a121 100644 --- a/packages/auth/src/Domain/UseCase/DeleteAccountsFromCSVFile/DeleteAccountsFromCSVFile.spec.ts +++ b/packages/auth/src/Domain/UseCase/DeleteAccountsFromCSVFile/DeleteAccountsFromCSVFile.spec.ts @@ -1,33 +1,51 @@ import { Logger } from 'winston' import { Result } from '@standardnotes/domain-core' +import { AccountDeletionVerificationRequestedEvent, DomainEventPublisherInterface } from '@standardnotes/domain-events' import { CSVFileReaderInterface } from '../../CSV/CSVFileReaderInterface' -import { DeleteAccount } from '../DeleteAccount/DeleteAccount' import { DeleteAccountsFromCSVFile } from './DeleteAccountsFromCSVFile' +import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' +import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface' +import { User } from '../../User/User' describe('DeleteAccountsFromCSVFile', () => { let csvFileReader: CSVFileReaderInterface - let deleteAccount: DeleteAccount + let userRepository: UserRepositoryInterface + let domainEventPublisher: DomainEventPublisherInterface + let domainEventFactory: DomainEventFactoryInterface let logger: Logger - const createUseCase = () => new DeleteAccountsFromCSVFile(csvFileReader, deleteAccount, logger) + const createUseCase = () => + new DeleteAccountsFromCSVFile(csvFileReader, domainEventPublisher, domainEventFactory, userRepository, logger) beforeEach(() => { + const user = {} as jest.Mocked + csvFileReader = {} as jest.Mocked csvFileReader.getValues = jest.fn().mockResolvedValue(Result.ok(['email1'])) - deleteAccount = {} as jest.Mocked - deleteAccount.execute = jest.fn().mockResolvedValue(Result.ok('')) + userRepository = {} as jest.Mocked + userRepository.findAllByUsernameOrEmail = jest.fn().mockResolvedValue([user]) + + domainEventPublisher = {} as jest.Mocked + domainEventPublisher.publish = jest.fn() + + domainEventFactory = {} as jest.Mocked + domainEventFactory.createAccountDeletionVerificationRequestedEvent = jest + .fn() + .mockReturnValue({} as jest.Mocked) logger = {} as jest.Mocked logger.info = jest.fn() }) - it('should delete accounts', async () => { + it('should request account deletion verification', async () => { const useCase = createUseCase() const result = await useCase.execute({ fileName: 'test.csv', dryRun: false }) + expect(domainEventPublisher.publish).toHaveBeenCalled() + expect(result.isFailed()).toBeFalsy() }) @@ -56,12 +74,12 @@ describe('DeleteAccountsFromCSVFile', () => { const result = await useCase.execute({ fileName: 'test.csv', dryRun: true }) - expect(deleteAccount.execute).not.toHaveBeenCalled() + expect(domainEventPublisher.publish).not.toHaveBeenCalled() expect(result.isFailed()).toBeFalsy() }) - it('should return error if delete account fails', async () => { - deleteAccount.execute = jest.fn().mockResolvedValue(Result.fail('Oops')) + it('should return error username is invalid', async () => { + csvFileReader.getValues = jest.fn().mockResolvedValue(Result.ok([''])) const useCase = createUseCase() @@ -69,4 +87,15 @@ describe('DeleteAccountsFromCSVFile', () => { expect(result.isFailed()).toBeTruthy() }) + + it('should do nothing if users could not be found', async () => { + userRepository.findAllByUsernameOrEmail = jest.fn().mockResolvedValue([]) + + const useCase = createUseCase() + + const result = await useCase.execute({ fileName: 'test.csv', dryRun: false }) + + expect(domainEventPublisher.publish).not.toHaveBeenCalled() + expect(result.isFailed()).toBeFalsy() + }) }) diff --git a/packages/auth/src/Domain/UseCase/DeleteAccountsFromCSVFile/DeleteAccountsFromCSVFile.ts b/packages/auth/src/Domain/UseCase/DeleteAccountsFromCSVFile/DeleteAccountsFromCSVFile.ts index b945c49f7..b494cc67b 100644 --- a/packages/auth/src/Domain/UseCase/DeleteAccountsFromCSVFile/DeleteAccountsFromCSVFile.ts +++ b/packages/auth/src/Domain/UseCase/DeleteAccountsFromCSVFile/DeleteAccountsFromCSVFile.ts @@ -1,14 +1,18 @@ -import { Result, UseCaseInterface } from '@standardnotes/domain-core' +import { Result, UseCaseInterface, Username } from '@standardnotes/domain-core' +import { DomainEventPublisherInterface } from '@standardnotes/domain-events' import { Logger } from 'winston' -import { DeleteAccount } from '../DeleteAccount/DeleteAccount' import { CSVFileReaderInterface } from '../../CSV/CSVFileReaderInterface' import { DeleteAccountsFromCSVFileDTO } from './DeleteAccountsFromCSVFileDTO' +import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface' +import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' export class DeleteAccountsFromCSVFile implements UseCaseInterface { constructor( private csvFileReader: CSVFileReaderInterface, - private deleteAccount: DeleteAccount, + private domainEventPublisher: DomainEventPublisherInterface, + private domainEventFactory: DomainEventFactoryInterface, + private userRepository: UserRepositoryInterface, private logger: Logger, ) {} @@ -33,12 +37,20 @@ export class DeleteAccountsFromCSVFile implements UseCaseInterface { } for (const email of emails) { - const deleteAccountOrError = await this.deleteAccount.execute({ - username: email, - }) + const usernameOrError = Username.create(email) + if (usernameOrError.isFailed()) { + return Result.fail(usernameOrError.getError()) + } + const username = usernameOrError.getValue() - if (deleteAccountOrError.isFailed()) { - return Result.fail(deleteAccountOrError.getError()) + const users = await this.userRepository.findAllByUsernameOrEmail(username) + for (const user of users) { + await this.domainEventPublisher.publish( + this.domainEventFactory.createAccountDeletionVerificationRequestedEvent({ + userUuid: user.uuid, + email: user.email, + }), + ) } } diff --git a/packages/auth/src/Domain/User/UserRepositoryInterface.ts b/packages/auth/src/Domain/User/UserRepositoryInterface.ts index 1346c1ef6..513b9361c 100644 --- a/packages/auth/src/Domain/User/UserRepositoryInterface.ts +++ b/packages/auth/src/Domain/User/UserRepositoryInterface.ts @@ -8,6 +8,7 @@ export interface UserRepositoryInterface { streamTeam(memberEmail?: Email): Promise findOneByUuid(uuid: Uuid): Promise findOneByUsernameOrEmail(usernameOrEmail: Email | Username): Promise + findAllByUsernameOrEmail(usernameOrEmail: Email | Username): Promise findAllCreatedBetween(dto: { start: Date; end: Date; offset: number; limit: number }): Promise countAllCreatedBetween(start: Date, end: Date): Promise save(user: User): Promise diff --git a/packages/auth/src/Infra/TypeORM/TypeORMUserRepository.ts b/packages/auth/src/Infra/TypeORM/TypeORMUserRepository.ts index 4f9e984cc..1920ba77b 100644 --- a/packages/auth/src/Infra/TypeORM/TypeORMUserRepository.ts +++ b/packages/auth/src/Infra/TypeORM/TypeORMUserRepository.ts @@ -69,7 +69,13 @@ export class TypeORMUserRepository implements UserRepositoryInterface { return this.ormRepository .createQueryBuilder('user') .where('user.email = :email', { email: usernameOrEmail.value }) - .cache(`user_email_${usernameOrEmail.value}`, 60000) .getOne() } + + async findAllByUsernameOrEmail(usernameOrEmail: Email | Username): Promise { + return this.ormRepository + .createQueryBuilder('user') + .where('user.email = :email', { email: usernameOrEmail.value }) + .getMany() + } } diff --git a/packages/domain-events/src/Domain/Event/AccountDeletionVerificationPassedEvent.ts b/packages/domain-events/src/Domain/Event/AccountDeletionVerificationPassedEvent.ts new file mode 100644 index 000000000..be1d9527c --- /dev/null +++ b/packages/domain-events/src/Domain/Event/AccountDeletionVerificationPassedEvent.ts @@ -0,0 +1,7 @@ +import { DomainEventInterface } from './DomainEventInterface' +import { AccountDeletionVerificationPassedEventPayload } from './AccountDeletionVerificationPassedEventPayload' + +export interface AccountDeletionVerificationPassedEvent extends DomainEventInterface { + type: 'ACCOUNT_DELETION_VERIFICATION_PASSED' + payload: AccountDeletionVerificationPassedEventPayload +} diff --git a/packages/domain-events/src/Domain/Event/AccountDeletionVerificationPassedEventPayload.ts b/packages/domain-events/src/Domain/Event/AccountDeletionVerificationPassedEventPayload.ts new file mode 100644 index 000000000..5b5a9c5b1 --- /dev/null +++ b/packages/domain-events/src/Domain/Event/AccountDeletionVerificationPassedEventPayload.ts @@ -0,0 +1,4 @@ +export interface AccountDeletionVerificationPassedEventPayload { + userUuid: string + email: string +} diff --git a/packages/domain-events/src/Domain/Event/AccountDeletionVerificationRequestedEvent.ts b/packages/domain-events/src/Domain/Event/AccountDeletionVerificationRequestedEvent.ts new file mode 100644 index 000000000..23a3946f4 --- /dev/null +++ b/packages/domain-events/src/Domain/Event/AccountDeletionVerificationRequestedEvent.ts @@ -0,0 +1,7 @@ +import { DomainEventInterface } from './DomainEventInterface' +import { AccountDeletionVerificationRequestedEventPayload } from './AccountDeletionVerificationRequestedEventPayload' + +export interface AccountDeletionVerificationRequestedEvent extends DomainEventInterface { + type: 'ACCOUNT_DELETION_VERIFICATION_REQUESTED' + payload: AccountDeletionVerificationRequestedEventPayload +} diff --git a/packages/domain-events/src/Domain/Event/AccountDeletionVerificationRequestedEventPayload.ts b/packages/domain-events/src/Domain/Event/AccountDeletionVerificationRequestedEventPayload.ts new file mode 100644 index 000000000..1d0213f72 --- /dev/null +++ b/packages/domain-events/src/Domain/Event/AccountDeletionVerificationRequestedEventPayload.ts @@ -0,0 +1,4 @@ +export interface AccountDeletionVerificationRequestedEventPayload { + userUuid: string + email: string +} diff --git a/packages/domain-events/src/Domain/index.ts b/packages/domain-events/src/Domain/index.ts index b1c1378cb..bed47423d 100644 --- a/packages/domain-events/src/Domain/index.ts +++ b/packages/domain-events/src/Domain/index.ts @@ -1,5 +1,9 @@ export * from './Event/AccountDeletionRequestedEvent' export * from './Event/AccountDeletionRequestedEventPayload' +export * from './Event/AccountDeletionVerificationPassedEvent' +export * from './Event/AccountDeletionVerificationPassedEventPayload' +export * from './Event/AccountDeletionVerificationRequestedEvent' +export * from './Event/AccountDeletionVerificationRequestedEventPayload' export * from './Event/DiscountApplyRequestedEvent' export * from './Event/DiscountApplyRequestedEventPayload' export * from './Event/DiscountWithdrawRequestedEvent' diff --git a/packages/syncing-server/src/Bootstrap/Container.ts b/packages/syncing-server/src/Bootstrap/Container.ts index 5431d1c3e..c14abe88b 100644 --- a/packages/syncing-server/src/Bootstrap/Container.ts +++ b/packages/syncing-server/src/Bootstrap/Container.ts @@ -160,6 +160,7 @@ import { DumpItem } from '../Domain/UseCase/Syncing/DumpItem/DumpItem' import { SyncResponse20200115 } from '../Domain/Item/SyncResponse/SyncResponse20200115' import { SyncResponse } from '@standardnotes/grpc' import { SyncResponseGRPCMapper } from '../Mapping/gRPC/SyncResponseGRPCMapper' +import { AccountDeletionVerificationRequestedEventHandler } from '../Domain/Handler/AccountDeletionVerificationRequestedEventHandler' export class ContainerConfigLoader { private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000 @@ -907,6 +908,18 @@ export class ContainerConfigLoader { container.get(TYPES.Sync_Logger), ), ) + container + .bind( + TYPES.Sync_AccountDeletionVerificationRequestedEventHandler, + ) + .toConstantValue( + new AccountDeletionVerificationRequestedEventHandler( + container.get(TYPES.Sync_SQLItemRepository), + container.get(TYPES.Sync_DomainEventPublisher), + container.get(TYPES.Sync_DomainEventFactory), + container.get(TYPES.Sync_Logger), + ), + ) container .bind(TYPES.Sync_ItemRevisionCreationRequestedEventHandler) .toConstantValue( @@ -957,6 +970,10 @@ export class ContainerConfigLoader { const eventHandlers: Map = new Map([ ['DUPLICATE_ITEM_SYNCED', container.get(TYPES.Sync_DuplicateItemSyncedEventHandler)], ['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.Sync_AccountDeletionRequestedEventHandler)], + [ + 'ACCOUNT_DELETION_VERIFICATION_REQUESTED', + container.get(TYPES.Sync_AccountDeletionVerificationRequestedEventHandler), + ], ['ITEM_REVISION_CREATION_REQUESTED', container.get(TYPES.Sync_ItemRevisionCreationRequestedEventHandler)], [ 'SHARED_VAULT_FILE_UPLOADED', diff --git a/packages/syncing-server/src/Bootstrap/Types.ts b/packages/syncing-server/src/Bootstrap/Types.ts index 81e6a50d1..cef5463be 100644 --- a/packages/syncing-server/src/Bootstrap/Types.ts +++ b/packages/syncing-server/src/Bootstrap/Types.ts @@ -85,6 +85,9 @@ const TYPES = { Sync_DumpItem: Symbol.for('Sync_DumpItem'), // Handlers Sync_AccountDeletionRequestedEventHandler: Symbol.for('Sync_AccountDeletionRequestedEventHandler'), + Sync_AccountDeletionVerificationRequestedEventHandler: Symbol.for( + 'Sync_AccountDeletionVerificationRequestedEventHandler', + ), Sync_DuplicateItemSyncedEventHandler: Symbol.for('Sync_DuplicateItemSyncedEventHandler'), Sync_EmailBackupRequestedEventHandler: Symbol.for('Sync_EmailBackupRequestedEventHandler'), Sync_ItemRevisionCreationRequestedEventHandler: Symbol.for('Sync_ItemRevisionCreationRequestedEventHandler'), diff --git a/packages/syncing-server/src/Domain/Event/DomainEventFactory.ts b/packages/syncing-server/src/Domain/Event/DomainEventFactory.ts index 6bda70379..599423dcf 100644 --- a/packages/syncing-server/src/Domain/Event/DomainEventFactory.ts +++ b/packages/syncing-server/src/Domain/Event/DomainEventFactory.ts @@ -1,5 +1,6 @@ /* istanbul ignore file */ import { + AccountDeletionVerificationPassedEvent, DomainEventService, DuplicateItemSyncedEvent, EmailRequestedEvent, @@ -22,6 +23,24 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface' export class DomainEventFactory implements DomainEventFactoryInterface { constructor(private timer: TimerInterface) {} + createAccountDeletionVerificationPassedEvent(dto: { + userUuid: string + email: string + }): AccountDeletionVerificationPassedEvent { + return { + type: 'ACCOUNT_DELETION_VERIFICATION_PASSED', + createdAt: this.timer.getUTCDate(), + meta: { + correlation: { + userIdentifier: dto.userUuid, + userIdentifierType: 'uuid', + }, + origin: DomainEventService.SyncingServer, + }, + payload: dto, + } + } + createUserDesignatedAsSurvivorInSharedVaultEvent(dto: { sharedVaultUuid: string userUuid: string diff --git a/packages/syncing-server/src/Domain/Event/DomainEventFactoryInterface.ts b/packages/syncing-server/src/Domain/Event/DomainEventFactoryInterface.ts index aed8cab7a..dfed376a3 100644 --- a/packages/syncing-server/src/Domain/Event/DomainEventFactoryInterface.ts +++ b/packages/syncing-server/src/Domain/Event/DomainEventFactoryInterface.ts @@ -1,4 +1,5 @@ import { + AccountDeletionVerificationPassedEvent, DuplicateItemSyncedEvent, EmailRequestedEvent, ItemDumpedEvent, @@ -93,4 +94,8 @@ export interface DomainEventFactoryInterface { userUuid: string timestamp: number }): UserDesignatedAsSurvivorInSharedVaultEvent + createAccountDeletionVerificationPassedEvent(dto: { + userUuid: string + email: string + }): AccountDeletionVerificationPassedEvent } diff --git a/packages/syncing-server/src/Domain/Handler/AccountDeletionVerificationRequestedEventHandler.ts b/packages/syncing-server/src/Domain/Handler/AccountDeletionVerificationRequestedEventHandler.ts new file mode 100644 index 000000000..95e0b5f10 --- /dev/null +++ b/packages/syncing-server/src/Domain/Handler/AccountDeletionVerificationRequestedEventHandler.ts @@ -0,0 +1,48 @@ +import { + AccountDeletionVerificationRequestedEvent, + DomainEventHandlerInterface, + DomainEventPublisherInterface, +} from '@standardnotes/domain-events' +import { Uuid } from '@standardnotes/domain-core' +import { Logger } from 'winston' + +import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface' +import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface' + +export class AccountDeletionVerificationRequestedEventHandler implements DomainEventHandlerInterface { + constructor( + private itemRepository: ItemRepositoryInterface, + private domainEventPublisher: DomainEventPublisherInterface, + private domainEventFactory: DomainEventFactoryInterface, + private logger: Logger, + ) {} + + async handle(event: AccountDeletionVerificationRequestedEvent): Promise { + const userUuidOrError = Uuid.create(event.payload.userUuid) + if (userUuidOrError.isFailed()) { + this.logger.error(`AccountDeletionVerificationRequestedEventHandler failed: ${userUuidOrError.getError()}`) + + return + } + const userUuid = userUuidOrError.getValue() + + const itemsCount = await this.itemRepository.countAll({ + userUuid: userUuid.value, + }) + + if (itemsCount !== 0) { + this.logger.warn( + `AccountDeletionVerificationRequestedEventHandler: User ${userUuid.value} has ${itemsCount} items and cannot be deleted.`, + ) + + return + } + + await this.domainEventPublisher.publish( + this.domainEventFactory.createAccountDeletionVerificationPassedEvent({ + userUuid: userUuid.value, + email: event.payload.email, + }), + ) + } +}