diff --git a/packages/revisions/src/Bootstrap/Container.ts b/packages/revisions/src/Bootstrap/Container.ts index 6dba3ba38..0e7d5f31e 100644 --- a/packages/revisions/src/Bootstrap/Container.ts +++ b/packages/revisions/src/Bootstrap/Container.ts @@ -38,6 +38,7 @@ import { S3DumpRepository } from '../Infra/S3/S3ItemDumpRepository' import { FSDumpRepository } from '../Infra/FS/FSDumpRepository' import { GetRevision } from '../Domain/UseCase/GetRevision/GetRevision' import { DeleteRevision } from '../Domain/UseCase/DeleteRevision/DeleteRevision' +import { AccountDeletionRequestedEventHandler } from '../Domain/Handler/AccountDeletionRequestedEventHandler' // eslint-disable-next-line @typescript-eslint/no-var-requires const newrelicFormatter = require('@newrelic/winston-enricher') @@ -179,6 +180,11 @@ export class ContainerConfigLoader { .toConstantValue( new ItemDumpedEventHandler(container.get(TYPES.DumpRepository), container.get(TYPES.RevisionRepository)), ) + container + .bind(TYPES.AccountDeletionRequestedEventHandler) + .toConstantValue( + new AccountDeletionRequestedEventHandler(container.get(TYPES.RevisionRepository), container.get(TYPES.Logger)), + ) // Services container @@ -192,6 +198,7 @@ export class ContainerConfigLoader { const eventHandlers: Map = new Map([ ['ITEM_DUMPED', container.get(TYPES.ItemDumpedEventHandler)], + ['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.AccountDeletionRequestedEventHandler)], ]) if (env.get('SQS_QUEUE_URL', true)) { diff --git a/packages/revisions/src/Bootstrap/Types.ts b/packages/revisions/src/Bootstrap/Types.ts index dc1b9474d..cc1c5642d 100644 --- a/packages/revisions/src/Bootstrap/Types.ts +++ b/packages/revisions/src/Bootstrap/Types.ts @@ -31,6 +31,7 @@ const TYPES = { RevisionsController: Symbol.for('RevisionsController'), // Handlers ItemDumpedEventHandler: Symbol.for('ItemDumpedEventHandler'), + AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'), // Services CrossServiceTokenDecoder: Symbol.for('CrossServiceTokenDecoder'), DomainEventSubscriberFactory: Symbol.for('DomainEventSubscriberFactory'), diff --git a/packages/revisions/src/Domain/Handler/AccountDeletionRequestedEventHandler.spec.ts b/packages/revisions/src/Domain/Handler/AccountDeletionRequestedEventHandler.spec.ts new file mode 100644 index 000000000..e43b07e0a --- /dev/null +++ b/packages/revisions/src/Domain/Handler/AccountDeletionRequestedEventHandler.spec.ts @@ -0,0 +1,45 @@ +import 'reflect-metadata' + +import { AccountDeletionRequestedEvent } from '@standardnotes/domain-events' +import { Logger } from 'winston' +import { AccountDeletionRequestedEventHandler } from './AccountDeletionRequestedEventHandler' +import { RevisionRepositoryInterface } from '../Revision/RevisionRepositoryInterface' + +describe('AccountDeletionRequestedEventHandler', () => { + let revisionRepository: RevisionRepositoryInterface + let logger: Logger + let event: AccountDeletionRequestedEvent + + const createHandler = () => new AccountDeletionRequestedEventHandler(revisionRepository, logger) + + beforeEach(() => { + revisionRepository = {} as jest.Mocked + revisionRepository.removeByUserUuid = jest.fn() + + logger = {} as jest.Mocked + logger.info = jest.fn() + logger.warn = jest.fn() + + event = {} as jest.Mocked + event.createdAt = new Date(1) + event.payload = { + userUuid: '2-3-4', + userCreatedAtTimestamp: 1, + regularSubscriptionUuid: '1-2-3', + } + }) + + it('should remove all revisions for a user', async () => { + event.payload.userUuid = '84c0f8e8-544a-4c7e-9adf-26209303bc1d' + + await createHandler().handle(event) + + expect(revisionRepository.removeByUserUuid).toHaveBeenCalled() + }) + + it('should not remove all revisions for an invalid user uuid', async () => { + await createHandler().handle(event) + + expect(revisionRepository.removeByUserUuid).not.toHaveBeenCalled() + }) +}) diff --git a/packages/revisions/src/Domain/Handler/AccountDeletionRequestedEventHandler.ts b/packages/revisions/src/Domain/Handler/AccountDeletionRequestedEventHandler.ts new file mode 100644 index 000000000..c727eb3ea --- /dev/null +++ b/packages/revisions/src/Domain/Handler/AccountDeletionRequestedEventHandler.ts @@ -0,0 +1,23 @@ +import { Uuid } from '@standardnotes/domain-core' +import { AccountDeletionRequestedEvent, DomainEventHandlerInterface } from '@standardnotes/domain-events' +import { Logger } from 'winston' + +import { RevisionRepositoryInterface } from '../Revision/RevisionRepositoryInterface' + +export class AccountDeletionRequestedEventHandler implements DomainEventHandlerInterface { + constructor(private revisionRepository: RevisionRepositoryInterface, private logger: Logger) {} + + async handle(event: AccountDeletionRequestedEvent): Promise { + const userUuidOrError = Uuid.create(event.payload.userUuid) + if (userUuidOrError.isFailed()) { + this.logger.warn(`Failed account cleanup: ${userUuidOrError.getError()}`) + + return + } + const userUuid = userUuidOrError.getValue() + + await this.revisionRepository.removeByUserUuid(userUuid) + + this.logger.info(`Finished account cleanup for user: ${event.payload.userUuid}`) + } +} diff --git a/packages/revisions/src/Domain/Revision/RevisionRepositoryInterface.ts b/packages/revisions/src/Domain/Revision/RevisionRepositoryInterface.ts index 2832c1fac..15797020a 100644 --- a/packages/revisions/src/Domain/Revision/RevisionRepositoryInterface.ts +++ b/packages/revisions/src/Domain/Revision/RevisionRepositoryInterface.ts @@ -4,6 +4,7 @@ import { Revision } from './Revision' import { RevisionMetadata } from './RevisionMetadata' export interface RevisionRepositoryInterface { + removeByUserUuid(userUuid: Uuid): Promise removeOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise findOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise findMetadataByItemId(itemUuid: Uuid, userUuid: Uuid): Promise> diff --git a/packages/revisions/src/Infra/MySQL/MySQLRevisionRepository.ts b/packages/revisions/src/Infra/MySQL/MySQLRevisionRepository.ts index 694c7452c..8cf3da231 100644 --- a/packages/revisions/src/Infra/MySQL/MySQLRevisionRepository.ts +++ b/packages/revisions/src/Infra/MySQL/MySQLRevisionRepository.ts @@ -13,6 +13,15 @@ export class MySQLRevisionRepository implements RevisionRepositoryInterface { private revisionMapper: MapperInterface, ) {} + async removeByUserUuid(userUuid: Uuid): Promise { + await this.ormRepository + .createQueryBuilder() + .delete() + .from('revisions') + .where('user_uuid = :userUuid', { userUuid }) + .execute() + } + async removeOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise { await this.ormRepository .createQueryBuilder()