From dc77ff3e45983d231bc9c132802428e77b4be431 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20S=C3=B3jko?= Date: Fri, 22 Sep 2023 10:49:53 +0200 Subject: [PATCH] feat: remove user from all shared vaults upon account deletion (#843) --- jest.config.js | 2 +- packages/auth/src/Bootstrap/Container.ts | 11 +- ...countDeletionRequestedEventHandler.spec.ts | 114 ------------------ .../AccountDeletionRequestedEventHandler.ts | 24 ++-- .../UserRemovedFromSharedVaultEventHandler.ts | 6 + .../RemoveSharedVaultUser.spec.ts | 12 ++ .../RemoveSharedVaultUser.ts | 36 ++++-- .../RemoveSharedVaultUserDTO.ts | 2 +- .../syncing-server/src/Bootstrap/Container.ts | 11 ++ .../syncing-server/src/Bootstrap/Types.ts | 1 + .../AccountDeletionRequestedEventHandler.ts | 21 +++- .../RemoveUserFromSharedVaults.spec.ts | 79 ++++++++++++ .../RemoveUserFromSharedVaults.ts | 41 +++++++ .../RemoveUserFromSharedVaultsDTO.ts | 3 + 14 files changed, 220 insertions(+), 143 deletions(-) delete mode 100644 packages/auth/src/Domain/Handler/AccountDeletionRequestedEventHandler.spec.ts create mode 100644 packages/syncing-server/src/Domain/UseCase/SharedVaults/RemoveUserFromSharedVaults/RemoveUserFromSharedVaults.spec.ts create mode 100644 packages/syncing-server/src/Domain/UseCase/SharedVaults/RemoveUserFromSharedVaults/RemoveUserFromSharedVaults.ts create mode 100644 packages/syncing-server/src/Domain/UseCase/SharedVaults/RemoveUserFromSharedVaults/RemoveUserFromSharedVaultsDTO.ts diff --git a/jest.config.js b/jest.config.js index e6b8ff0e8..b0857c1ae 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,7 +3,7 @@ module.exports = { testEnvironment: 'node', testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.ts$', testTimeout: 20000, - coverageReporters: ['text-summary'], + coverageReporters: ['text'], reporters: ['summary'], coverageThreshold: { global: { diff --git a/packages/auth/src/Bootstrap/Container.ts b/packages/auth/src/Bootstrap/Container.ts index 08b39e53f..a52d498e0 100644 --- a/packages/auth/src/Bootstrap/Container.ts +++ b/packages/auth/src/Bootstrap/Container.ts @@ -1008,7 +1008,16 @@ export class ContainerConfigLoader { container.bind(TYPES.Auth_UserRegisteredEventHandler).to(UserRegisteredEventHandler) container .bind(TYPES.Auth_AccountDeletionRequestedEventHandler) - .to(AccountDeletionRequestedEventHandler) + .toConstantValue( + new AccountDeletionRequestedEventHandler( + container.get(TYPES.Auth_UserRepository), + container.get(TYPES.Auth_SessionRepository), + container.get(TYPES.Auth_EphemeralSessionRepository), + container.get(TYPES.Auth_RevokedSessionRepository), + container.get(TYPES.Auth_RemoveSharedVaultUser), + container.get(TYPES.Auth_Logger), + ), + ) container .bind(TYPES.Auth_SubscriptionPurchasedEventHandler) .to(SubscriptionPurchasedEventHandler) diff --git a/packages/auth/src/Domain/Handler/AccountDeletionRequestedEventHandler.spec.ts b/packages/auth/src/Domain/Handler/AccountDeletionRequestedEventHandler.spec.ts deleted file mode 100644 index edc9b02e4..000000000 --- a/packages/auth/src/Domain/Handler/AccountDeletionRequestedEventHandler.spec.ts +++ /dev/null @@ -1,114 +0,0 @@ -import 'reflect-metadata' - -import { AccountDeletionRequestedEvent } from '@standardnotes/domain-events' -import { Logger } from 'winston' -import { EphemeralSession } from '../Session/EphemeralSession' -import { EphemeralSessionRepositoryInterface } from '../Session/EphemeralSessionRepositoryInterface' -import { RevokedSession } from '../Session/RevokedSession' -import { RevokedSessionRepositoryInterface } from '../Session/RevokedSessionRepositoryInterface' -import { Session } from '../Session/Session' -import { SessionRepositoryInterface } from '../Session/SessionRepositoryInterface' -import { User } from '../User/User' -import { UserRepositoryInterface } from '../User/UserRepositoryInterface' -import { AccountDeletionRequestedEventHandler } from './AccountDeletionRequestedEventHandler' - -describe('AccountDeletionRequestedEventHandler', () => { - let userRepository: UserRepositoryInterface - let sessionRepository: SessionRepositoryInterface - let ephemeralSessionRepository: EphemeralSessionRepositoryInterface - let revokedSessionRepository: RevokedSessionRepositoryInterface - let logger: Logger - let session: Session - let ephemeralSession: EphemeralSession - let revokedSession: RevokedSession - let user: User - let event: AccountDeletionRequestedEvent - - const createHandler = () => - new AccountDeletionRequestedEventHandler( - userRepository, - sessionRepository, - ephemeralSessionRepository, - revokedSessionRepository, - logger, - ) - - beforeEach(() => { - user = {} as jest.Mocked - - userRepository = {} as jest.Mocked - userRepository.findOneByUuid = jest.fn().mockReturnValue(user) - userRepository.remove = jest.fn() - - session = { - uuid: '1-2-3', - } as jest.Mocked - - sessionRepository = {} as jest.Mocked - sessionRepository.findAllByUserUuid = jest.fn().mockReturnValue([session]) - sessionRepository.remove = jest.fn() - - ephemeralSession = { - uuid: '2-3-4', - userUuid: '00000000-0000-0000-0000-000000000000', - } as jest.Mocked - - ephemeralSessionRepository = {} as jest.Mocked - ephemeralSessionRepository.findAllByUserUuid = jest.fn().mockReturnValue([ephemeralSession]) - ephemeralSessionRepository.deleteOne = jest.fn() - - revokedSession = { - uuid: '3-4-5', - } as jest.Mocked - - revokedSessionRepository = {} as jest.Mocked - revokedSessionRepository.findAllByUserUuid = jest.fn().mockReturnValue([revokedSession]) - revokedSessionRepository.remove = jest.fn() - - event = {} as jest.Mocked - event.createdAt = new Date(1) - event.payload = { - userUuid: '00000000-0000-0000-0000-000000000000', - userCreatedAtTimestamp: 1, - regularSubscriptionUuid: '2-3-4', - roleNames: ['CORE_USER'], - } - - logger = {} as jest.Mocked - logger.info = jest.fn() - logger.warn = jest.fn() - }) - - it('should remove a user', async () => { - await createHandler().handle(event) - - expect(userRepository.remove).toHaveBeenCalledWith(user) - }) - - it('should not remove a user with invalid uuid', async () => { - event.payload.userUuid = 'invalid' - - await createHandler().handle(event) - - expect(userRepository.remove).not.toHaveBeenCalled() - }) - - it('should not remove a user if one does not exist', async () => { - userRepository.findOneByUuid = jest.fn().mockReturnValue(null) - - await createHandler().handle(event) - - expect(userRepository.remove).not.toHaveBeenCalled() - expect(sessionRepository.remove).not.toHaveBeenCalled() - expect(revokedSessionRepository.remove).not.toHaveBeenCalled() - expect(ephemeralSessionRepository.deleteOne).not.toHaveBeenCalled() - }) - - it('should remove all user sessions', async () => { - await createHandler().handle(event) - - expect(sessionRepository.remove).toHaveBeenCalledWith(session) - expect(revokedSessionRepository.remove).toHaveBeenCalledWith(revokedSession) - expect(ephemeralSessionRepository.deleteOne).toHaveBeenCalledWith('2-3-4', '00000000-0000-0000-0000-000000000000') - }) -}) diff --git a/packages/auth/src/Domain/Handler/AccountDeletionRequestedEventHandler.ts b/packages/auth/src/Domain/Handler/AccountDeletionRequestedEventHandler.ts index 13ba3fcaa..af8501595 100644 --- a/packages/auth/src/Domain/Handler/AccountDeletionRequestedEventHandler.ts +++ b/packages/auth/src/Domain/Handler/AccountDeletionRequestedEventHandler.ts @@ -1,22 +1,21 @@ import { AccountDeletionRequestedEvent, DomainEventHandlerInterface } from '@standardnotes/domain-events' -import { inject, injectable } from 'inversify' +import { Uuid } from '@standardnotes/domain-core' import { Logger } from 'winston' -import TYPES from '../../Bootstrap/Types' + import { EphemeralSessionRepositoryInterface } from '../Session/EphemeralSessionRepositoryInterface' import { RevokedSessionRepositoryInterface } from '../Session/RevokedSessionRepositoryInterface' import { SessionRepositoryInterface } from '../Session/SessionRepositoryInterface' import { UserRepositoryInterface } from '../User/UserRepositoryInterface' -import { Uuid } from '@standardnotes/domain-core' +import { RemoveSharedVaultUser } from '../UseCase/RemoveSharedVaultUser/RemoveSharedVaultUser' -@injectable() export class AccountDeletionRequestedEventHandler implements DomainEventHandlerInterface { constructor( - @inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface, - @inject(TYPES.Auth_SessionRepository) private sessionRepository: SessionRepositoryInterface, - @inject(TYPES.Auth_EphemeralSessionRepository) + private userRepository: UserRepositoryInterface, + private sessionRepository: SessionRepositoryInterface, private ephemeralSessionRepository: EphemeralSessionRepositoryInterface, - @inject(TYPES.Auth_RevokedSessionRepository) private revokedSessionRepository: RevokedSessionRepositoryInterface, - @inject(TYPES.Auth_Logger) private logger: Logger, + private revokedSessionRepository: RevokedSessionRepositoryInterface, + private removeSharedVaultUser: RemoveSharedVaultUser, + private logger: Logger, ) {} async handle(event: AccountDeletionRequestedEvent): Promise { @@ -38,6 +37,13 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI await this.removeSessions(userUuid.value) + const result = await this.removeSharedVaultUser.execute({ + userUuid: userUuid.value, + }) + if (result.isFailed()) { + this.logger.error(`Could not remove shared vault user: ${result.getError()}`) + } + await this.userRepository.remove(user) this.logger.info(`Finished account cleanup for user: ${userUuid.value}`) diff --git a/packages/auth/src/Domain/Handler/UserRemovedFromSharedVaultEventHandler.ts b/packages/auth/src/Domain/Handler/UserRemovedFromSharedVaultEventHandler.ts index c70567aad..582db2aab 100644 --- a/packages/auth/src/Domain/Handler/UserRemovedFromSharedVaultEventHandler.ts +++ b/packages/auth/src/Domain/Handler/UserRemovedFromSharedVaultEventHandler.ts @@ -10,6 +10,12 @@ export class UserRemovedFromSharedVaultEventHandler implements DomainEventHandle ) {} async handle(event: UserRemovedFromSharedVaultEvent): Promise { + if (!event.payload.sharedVaultUuid) { + this.logger.error(`Shared vault uuid is missing from event: ${JSON.stringify(event)}`) + + return + } + const result = await this.removeSharedVaultUser.execute({ userUuid: event.payload.userUuid, sharedVaultUuid: event.payload.sharedVaultUuid, diff --git a/packages/auth/src/Domain/UseCase/RemoveSharedVaultUser/RemoveSharedVaultUser.spec.ts b/packages/auth/src/Domain/UseCase/RemoveSharedVaultUser/RemoveSharedVaultUser.spec.ts index 4db3624a2..6014144ff 100644 --- a/packages/auth/src/Domain/UseCase/RemoveSharedVaultUser/RemoveSharedVaultUser.spec.ts +++ b/packages/auth/src/Domain/UseCase/RemoveSharedVaultUser/RemoveSharedVaultUser.spec.ts @@ -13,6 +13,7 @@ describe('RemoveSharedVaultUser', () => { sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest .fn() .mockReturnValue({} as jest.Mocked) + sharedVaultUserRepository.findByUserUuid = jest.fn().mockReturnValue([{} as jest.Mocked]) sharedVaultUserRepository.remove = jest.fn() }) @@ -28,6 +29,17 @@ describe('RemoveSharedVaultUser', () => { expect(sharedVaultUserRepository.remove).toHaveBeenCalled() }) + it('should remove all shared vault users', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + }) + + expect(result.isFailed()).toBeFalsy() + expect(sharedVaultUserRepository.remove).toHaveBeenCalled() + }) + it('should fail when user uuid is invalid', async () => { const useCase = createUseCase() diff --git a/packages/auth/src/Domain/UseCase/RemoveSharedVaultUser/RemoveSharedVaultUser.ts b/packages/auth/src/Domain/UseCase/RemoveSharedVaultUser/RemoveSharedVaultUser.ts index afccd6bd1..0252ddec3 100644 --- a/packages/auth/src/Domain/UseCase/RemoveSharedVaultUser/RemoveSharedVaultUser.ts +++ b/packages/auth/src/Domain/UseCase/RemoveSharedVaultUser/RemoveSharedVaultUser.ts @@ -13,21 +13,31 @@ export class RemoveSharedVaultUser implements UseCaseInterface { } const userUuid = userUuidOrError.getValue() - const sharedVaultUuidOrError = Uuid.create(dto.sharedVaultUuid) - if (sharedVaultUuidOrError.isFailed()) { - return Result.fail(sharedVaultUuidOrError.getError()) - } - const sharedVaultUuid = sharedVaultUuidOrError.getValue() - - const sharedVaultUser = await this.sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid({ - userUuid, - sharedVaultUuid, - }) - if (!sharedVaultUser) { - return Result.fail('Shared vault user not found') + let sharedVaultUuid: Uuid | undefined + if (dto.sharedVaultUuid !== undefined) { + const sharedVaultUuidOrError = Uuid.create(dto.sharedVaultUuid) + if (sharedVaultUuidOrError.isFailed()) { + return Result.fail(sharedVaultUuidOrError.getError()) + } + sharedVaultUuid = sharedVaultUuidOrError.getValue() } - await this.sharedVaultUserRepository.remove(sharedVaultUser) + if (sharedVaultUuid) { + const sharedVaultUser = await this.sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid({ + userUuid, + sharedVaultUuid, + }) + if (!sharedVaultUser) { + return Result.fail('Shared vault user not found') + } + + await this.sharedVaultUserRepository.remove(sharedVaultUser) + } else { + const sharedVaultUsers = await this.sharedVaultUserRepository.findByUserUuid(userUuid) + for (const sharedVaultUser of sharedVaultUsers) { + await this.sharedVaultUserRepository.remove(sharedVaultUser) + } + } return Result.ok() } diff --git a/packages/auth/src/Domain/UseCase/RemoveSharedVaultUser/RemoveSharedVaultUserDTO.ts b/packages/auth/src/Domain/UseCase/RemoveSharedVaultUser/RemoveSharedVaultUserDTO.ts index ac3970af4..d4ae0adca 100644 --- a/packages/auth/src/Domain/UseCase/RemoveSharedVaultUser/RemoveSharedVaultUserDTO.ts +++ b/packages/auth/src/Domain/UseCase/RemoveSharedVaultUser/RemoveSharedVaultUserDTO.ts @@ -1,4 +1,4 @@ export interface RemoveSharedVaultUserDTO { - sharedVaultUuid: string + sharedVaultUuid?: string userUuid: string } diff --git a/packages/syncing-server/src/Bootstrap/Container.ts b/packages/syncing-server/src/Bootstrap/Container.ts index 1bbf3718a..e2b47a28a 100644 --- a/packages/syncing-server/src/Bootstrap/Container.ts +++ b/packages/syncing-server/src/Bootstrap/Container.ts @@ -169,6 +169,7 @@ import { DeleteSharedVaults } from '../Domain/UseCase/SharedVaults/DeleteSharedV import { RemoveItemsFromSharedVault } from '../Domain/UseCase/SharedVaults/RemoveItemsFromSharedVault/RemoveItemsFromSharedVault' import { SharedVaultRemovedEventHandler } from '../Domain/Handler/SharedVaultRemovedEventHandler' import { DesignateSurvivor } from '../Domain/UseCase/SharedVaults/DesignateSurvivor/DesignateSurvivor' +import { RemoveUserFromSharedVaults } from '../Domain/UseCase/SharedVaults/RemoveUserFromSharedVaults/RemoveUserFromSharedVaults' export class ContainerConfigLoader { private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000 @@ -876,6 +877,15 @@ export class ContainerConfigLoader { container.get(TYPES.Sync_DomainEventPublisher), ), ) + container + .bind(TYPES.Sync_RemoveUserFromSharedVaults) + .toConstantValue( + new RemoveUserFromSharedVaults( + container.get(TYPES.Sync_SharedVaultUserRepository), + container.get(TYPES.Sync_RemoveSharedVaultUser), + container.get(TYPES.Sync_Logger), + ), + ) // Services container @@ -938,6 +948,7 @@ export class ContainerConfigLoader { new AccountDeletionRequestedEventHandler( container.get(TYPES.Sync_ItemRepositoryResolver), container.get(TYPES.Sync_DeleteSharedVaults), + container.get(TYPES.Sync_RemoveUserFromSharedVaults), container.get(TYPES.Sync_Logger), ), ) diff --git a/packages/syncing-server/src/Bootstrap/Types.ts b/packages/syncing-server/src/Bootstrap/Types.ts index d8ede1348..cbd06f252 100644 --- a/packages/syncing-server/src/Bootstrap/Types.ts +++ b/packages/syncing-server/src/Bootstrap/Types.ts @@ -88,6 +88,7 @@ const TYPES = { Sync_SendEventToClient: Symbol.for('Sync_SendEventToClient'), Sync_RemoveItemsFromSharedVault: Symbol.for('Sync_RemoveItemsFromSharedVault'), Sync_DesignateSurvivor: Symbol.for('Sync_DesignateSurvivor'), + Sync_RemoveUserFromSharedVaults: Symbol.for('Sync_RemoveUserFromSharedVaults'), // Handlers Sync_AccountDeletionRequestedEventHandler: Symbol.for('Sync_AccountDeletionRequestedEventHandler'), Sync_DuplicateItemSyncedEventHandler: Symbol.for('Sync_DuplicateItemSyncedEventHandler'), diff --git a/packages/syncing-server/src/Domain/Handler/AccountDeletionRequestedEventHandler.ts b/packages/syncing-server/src/Domain/Handler/AccountDeletionRequestedEventHandler.ts index 9055d726c..1971e7fcd 100644 --- a/packages/syncing-server/src/Domain/Handler/AccountDeletionRequestedEventHandler.ts +++ b/packages/syncing-server/src/Domain/Handler/AccountDeletionRequestedEventHandler.ts @@ -4,11 +4,13 @@ import { Logger } from 'winston' import { ItemRepositoryResolverInterface } from '../Item/ItemRepositoryResolverInterface' import { DeleteSharedVaults } from '../UseCase/SharedVaults/DeleteSharedVaults/DeleteSharedVaults' +import { RemoveUserFromSharedVaults } from '../UseCase/SharedVaults/RemoveUserFromSharedVaults/RemoveUserFromSharedVaults' export class AccountDeletionRequestedEventHandler implements DomainEventHandlerInterface { constructor( private itemRepositoryResolver: ItemRepositoryResolverInterface, private deleteSharedVaults: DeleteSharedVaults, + private removeUserFromSharedVaults: RemoveUserFromSharedVaults, private logger: Logger, ) {} @@ -23,13 +25,24 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI await itemRepository.deleteByUserUuid(event.payload.userUuid) - const result = await this.deleteSharedVaults.execute({ + const deletingVaultsResult = await this.deleteSharedVaults.execute({ ownerUuid: event.payload.userUuid, }) - if (result.isFailed()) { - this.logger.error(`Failed to delete shared vaults for user: ${event.payload.userUuid}: ${result.getError()}`) + if (deletingVaultsResult.isFailed()) { + this.logger.error( + `Failed to delete shared vaults for user: ${event.payload.userUuid}: ${deletingVaultsResult.getError()}`, + ) + } - return + const deletingUserFromOtherVaultsResult = await this.removeUserFromSharedVaults.execute({ + userUuid: event.payload.userUuid, + }) + if (deletingUserFromOtherVaultsResult.isFailed()) { + this.logger.error( + `Failed to remove user: ${ + event.payload.userUuid + } from shared vaults: ${deletingUserFromOtherVaultsResult.getError()}`, + ) } this.logger.info(`Finished account cleanup for user: ${event.payload.userUuid}`) diff --git a/packages/syncing-server/src/Domain/UseCase/SharedVaults/RemoveUserFromSharedVaults/RemoveUserFromSharedVaults.spec.ts b/packages/syncing-server/src/Domain/UseCase/SharedVaults/RemoveUserFromSharedVaults/RemoveUserFromSharedVaults.spec.ts new file mode 100644 index 000000000..5607ae2fc --- /dev/null +++ b/packages/syncing-server/src/Domain/UseCase/SharedVaults/RemoveUserFromSharedVaults/RemoveUserFromSharedVaults.spec.ts @@ -0,0 +1,79 @@ +import { Result, SharedVaultUser, SharedVaultUserPermission, Timestamps, Uuid } from '@standardnotes/domain-core' +import { Logger } from 'winston' + +import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface' +import { RemoveUserFromSharedVault } from '../RemoveUserFromSharedVault/RemoveUserFromSharedVault' +import { RemoveUserFromSharedVaults } from './RemoveUserFromSharedVaults' + +describe('RemoveUserFromSharedVaults', () => { + let sharedVaultUserRepository: SharedVaultUserRepositoryInterface + let sharedVaultUser: SharedVaultUser + let removeUserFromSharedVault: RemoveUserFromSharedVault + let logger: Logger + + const createUseCase = () => + new RemoveUserFromSharedVaults(sharedVaultUserRepository, removeUserFromSharedVault, logger) + + beforeEach(() => { + sharedVaultUser = SharedVaultUser.create({ + permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Write).getValue(), + sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + timestamps: Timestamps.create(123, 123).getValue(), + isDesignatedSurvivor: false, + }).getValue() + + sharedVaultUserRepository = {} as jest.Mocked + sharedVaultUserRepository.findByUserUuid = jest.fn().mockResolvedValue([sharedVaultUser]) + + removeUserFromSharedVault = {} as jest.Mocked + removeUserFromSharedVault.execute = jest.fn().mockResolvedValue(Result.ok()) + + logger = {} as jest.Mocked + logger.error = jest.fn() + }) + + it('should remove user from shared vaults', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + }) + + expect(result.isFailed()).toBeFalsy() + expect(removeUserFromSharedVault.execute).toHaveBeenCalledTimes(1) + expect(removeUserFromSharedVault.execute).toHaveBeenCalledWith({ + sharedVaultUuid: '00000000-0000-0000-0000-000000000000', + originatorUuid: '00000000-0000-0000-0000-000000000000', + userUuid: '00000000-0000-0000-0000-000000000000', + forceRemoveOwner: true, + }) + }) + + it('should log error if removing user from shared vault fails', async () => { + removeUserFromSharedVault.execute = jest.fn().mockResolvedValue(Result.fail('error')) + + const useCase = createUseCase() + + const result = await useCase.execute({ + userUuid: '00000000-0000-0000-0000-000000000000', + }) + + expect(result.isFailed()).toBeFalsy() + expect(logger.error).toHaveBeenCalledTimes(1) + expect(logger.error).toHaveBeenCalledWith( + 'Failed to remove user: 00000000-0000-0000-0000-000000000000 from shared vault: 00000000-0000-0000-0000-000000000000: error', + ) + }) + + it('should fail if the user uuid is invalid', async () => { + const useCase = createUseCase() + + const result = await useCase.execute({ + userUuid: 'invalid', + }) + + expect(result.isFailed()).toBeTruthy() + expect(removeUserFromSharedVault.execute).not.toHaveBeenCalled() + }) +}) diff --git a/packages/syncing-server/src/Domain/UseCase/SharedVaults/RemoveUserFromSharedVaults/RemoveUserFromSharedVaults.ts b/packages/syncing-server/src/Domain/UseCase/SharedVaults/RemoveUserFromSharedVaults/RemoveUserFromSharedVaults.ts new file mode 100644 index 000000000..1bc9dde05 --- /dev/null +++ b/packages/syncing-server/src/Domain/UseCase/SharedVaults/RemoveUserFromSharedVaults/RemoveUserFromSharedVaults.ts @@ -0,0 +1,41 @@ +import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core' +import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface' +import { RemoveUserFromSharedVault } from '../RemoveUserFromSharedVault/RemoveUserFromSharedVault' +import { Logger } from 'winston' +import { RemoveUserFromSharedVaultsDTO } from './RemoveUserFromSharedVaultsDTO' + +export class RemoveUserFromSharedVaults implements UseCaseInterface { + constructor( + private sharedVaultUserRepository: SharedVaultUserRepositoryInterface, + private removeUserFromSharedVault: RemoveUserFromSharedVault, + private logger: Logger, + ) {} + + async execute(dto: RemoveUserFromSharedVaultsDTO): Promise> { + const userUuidOrError = Uuid.create(dto.userUuid) + if (userUuidOrError.isFailed()) { + return Result.fail(userUuidOrError.getError()) + } + const userUuid = userUuidOrError.getValue() + + const sharedVaultUsers = await this.sharedVaultUserRepository.findByUserUuid(userUuid) + for (const sharedVaultUser of sharedVaultUsers) { + const result = await this.removeUserFromSharedVault.execute({ + sharedVaultUuid: sharedVaultUser.props.sharedVaultUuid.value, + originatorUuid: userUuid.value, + userUuid: userUuid.value, + forceRemoveOwner: true, + }) + + if (result.isFailed()) { + this.logger.error( + `Failed to remove user: ${userUuid.value} from shared vault: ${ + sharedVaultUser.props.sharedVaultUuid.value + }: ${result.getError()}`, + ) + } + } + + return Result.ok() + } +} diff --git a/packages/syncing-server/src/Domain/UseCase/SharedVaults/RemoveUserFromSharedVaults/RemoveUserFromSharedVaultsDTO.ts b/packages/syncing-server/src/Domain/UseCase/SharedVaults/RemoveUserFromSharedVaults/RemoveUserFromSharedVaultsDTO.ts new file mode 100644 index 000000000..dbfa9e481 --- /dev/null +++ b/packages/syncing-server/src/Domain/UseCase/SharedVaults/RemoveUserFromSharedVaults/RemoveUserFromSharedVaultsDTO.ts @@ -0,0 +1,3 @@ +export interface RemoveUserFromSharedVaultsDTO { + userUuid: string +}