feat: add verifiying if user has no items before mass deleting spam accounts (#936)
This commit is contained in:
parent
4d12566b0d
commit
c11abe1bd3
19 changed files with 243 additions and 19 deletions
|
@ -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<CSVFileReaderInterface>(TYPES.Auth_CSVFileReader),
|
||||
container.get<DeleteAccount>(TYPES.Auth_DeleteAccount),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory),
|
||||
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
|
@ -1328,6 +1331,14 @@ export class ContainerConfigLoader {
|
|||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<AccountDeletionVerificationPassedEventHandler>(TYPES.Auth_AccountDeletionVerificationPassedEventHandler)
|
||||
.toConstantValue(
|
||||
new AccountDeletionVerificationPassedEventHandler(
|
||||
container.get<DeleteAccount>(TYPES.Auth_DeleteAccount),
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SubscriptionPurchasedEventHandler>(TYPES.Auth_SubscriptionPurchasedEventHandler)
|
||||
.toConstantValue(
|
||||
|
@ -1516,6 +1527,7 @@ export class ContainerConfigLoader {
|
|||
|
||||
const eventHandlers: Map<string, DomainEventHandlerInterface> = 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)],
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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<void> {
|
||||
const result = await this.deleteAccount.execute({
|
||||
userUuid: event.payload.userUuid,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(`AccountDeletionVerificationPassedEventHandler failed: ${result.getError()}`)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<User>
|
||||
|
||||
csvFileReader = {} as jest.Mocked<CSVFileReaderInterface>
|
||||
csvFileReader.getValues = jest.fn().mockResolvedValue(Result.ok(['email1']))
|
||||
|
||||
deleteAccount = {} as jest.Mocked<DeleteAccount>
|
||||
deleteAccount.execute = jest.fn().mockResolvedValue(Result.ok(''))
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findAllByUsernameOrEmail = jest.fn().mockResolvedValue([user])
|
||||
|
||||
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
|
||||
domainEventPublisher.publish = jest.fn()
|
||||
|
||||
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
||||
domainEventFactory.createAccountDeletionVerificationRequestedEvent = jest
|
||||
.fn()
|
||||
.mockReturnValue({} as jest.Mocked<AccountDeletionVerificationRequestedEvent>)
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
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()
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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<void> {
|
||||
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<void> {
|
|||
}
|
||||
|
||||
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,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ export interface UserRepositoryInterface {
|
|||
streamTeam(memberEmail?: Email): Promise<ReadStream>
|
||||
findOneByUuid(uuid: Uuid): Promise<User | null>
|
||||
findOneByUsernameOrEmail(usernameOrEmail: Email | Username): Promise<User | null>
|
||||
findAllByUsernameOrEmail(usernameOrEmail: Email | Username): Promise<User[]>
|
||||
findAllCreatedBetween(dto: { start: Date; end: Date; offset: number; limit: number }): Promise<User[]>
|
||||
countAllCreatedBetween(start: Date, end: Date): Promise<number>
|
||||
save(user: User): Promise<User>
|
||||
|
|
|
@ -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<User[]> {
|
||||
return this.ormRepository
|
||||
.createQueryBuilder('user')
|
||||
.where('user.email = :email', { email: usernameOrEmail.value })
|
||||
.getMany()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { AccountDeletionVerificationPassedEventPayload } from './AccountDeletionVerificationPassedEventPayload'
|
||||
|
||||
export interface AccountDeletionVerificationPassedEvent extends DomainEventInterface {
|
||||
type: 'ACCOUNT_DELETION_VERIFICATION_PASSED'
|
||||
payload: AccountDeletionVerificationPassedEventPayload
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export interface AccountDeletionVerificationPassedEventPayload {
|
||||
userUuid: string
|
||||
email: string
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { AccountDeletionVerificationRequestedEventPayload } from './AccountDeletionVerificationRequestedEventPayload'
|
||||
|
||||
export interface AccountDeletionVerificationRequestedEvent extends DomainEventInterface {
|
||||
type: 'ACCOUNT_DELETION_VERIFICATION_REQUESTED'
|
||||
payload: AccountDeletionVerificationRequestedEventPayload
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export interface AccountDeletionVerificationRequestedEventPayload {
|
||||
userUuid: string
|
||||
email: string
|
||||
}
|
|
@ -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'
|
||||
|
|
|
@ -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<Logger>(TYPES.Sync_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<AccountDeletionVerificationRequestedEventHandler>(
|
||||
TYPES.Sync_AccountDeletionVerificationRequestedEventHandler,
|
||||
)
|
||||
.toConstantValue(
|
||||
new AccountDeletionVerificationRequestedEventHandler(
|
||||
container.get<ItemRepositoryInterface>(TYPES.Sync_SQLItemRepository),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
|
||||
container.get<Logger>(TYPES.Sync_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<ItemRevisionCreationRequestedEventHandler>(TYPES.Sync_ItemRevisionCreationRequestedEventHandler)
|
||||
.toConstantValue(
|
||||
|
@ -957,6 +970,10 @@ export class ContainerConfigLoader {
|
|||
const eventHandlers: Map<string, DomainEventHandlerInterface> = 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',
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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<void> {
|
||||
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,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue