瀏覽代碼

fix: removing items in a vault and notifying about designated survivor (#855)

* fix: removing items in a vault and notifying about designated survivor

* fix deleting shared vault items
Karol Sójko 1 年之前
父節點
當前提交
1d06ffe9d5

+ 1 - 0
packages/domain-core/src/Domain/Notification/NotificationPayloadIdentifierType.ts

@@ -5,6 +5,7 @@ import { NotificationPayloadIdentifierTypeProps } from './NotificationPayloadIde
 export class NotificationPayloadIdentifierType extends ValueObject<NotificationPayloadIdentifierTypeProps> {
   static readonly TYPES = {
     SharedVaultUuid: 'shared_vault_uuid',
+    UserUuid: 'user_uuid',
     SharedVaultInviteUuid: 'shared_vault_invite_uuid',
     ItemUuid: 'item_uuid',
   }

+ 1 - 0
packages/domain-core/src/Domain/Notification/NotificationType.ts

@@ -7,6 +7,7 @@ export class NotificationType extends ValueObject<NotificationTypeProps> {
     SharedVaultItemRemoved: 'shared_vault_item_removed',
     SelfRemovedFromSharedVault: 'self_removed_from_shared_vault',
     UserRemovedFromSharedVault: 'user_removed_from_shared_vault',
+    UserDesignatedAsSurvivor: 'user_designated_as_survivor',
     UserAddedToSharedVault: 'user_added_to_shared_vault',
     SharedVaultInviteCanceled: 'shared_vault_invite_canceled',
     SharedVaultFileUploaded: 'shared_vault_file_uploaded',

+ 1 - 0
packages/syncing-server/src/Bootstrap/Container.ts

@@ -899,6 +899,7 @@ export class ContainerConfigLoader {
           container.get<TimerInterface>(TYPES.Sync_Timer),
           container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
           container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
+          container.get<AddNotificationForUser>(TYPES.Sync_AddNotificationForUser),
         ),
       )
     container

+ 22 - 2
packages/syncing-server/src/Domain/Handler/AccountDeletionRequestedEventHandler.ts

@@ -1,5 +1,5 @@
 import { AccountDeletionRequestedEvent, DomainEventHandlerInterface } from '@standardnotes/domain-events'
-import { RoleNameCollection } from '@standardnotes/domain-core'
+import { RoleNameCollection, Uuid } from '@standardnotes/domain-core'
 import { Logger } from 'winston'
 
 import { ItemRepositoryResolverInterface } from '../Item/ItemRepositoryResolverInterface'
@@ -15,15 +15,25 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
   ) {}
 
   async handle(event: AccountDeletionRequestedEvent): Promise<void> {
+    const userUuidOrError = Uuid.create(event.payload.userUuid)
+    if (userUuidOrError.isFailed()) {
+      this.logger.error(`AccountDeletionRequestedEventHandler failed: ${userUuidOrError.getError()}`)
+
+      return
+    }
+    const userUuid = userUuidOrError.getValue()
+
     const roleNamesOrError = RoleNameCollection.create(event.payload.roleNames)
     if (roleNamesOrError.isFailed()) {
+      this.logger.error(`AccountDeletionRequestedEventHandler failed: ${roleNamesOrError.getError()}`)
+
       return
     }
     const roleNames = roleNamesOrError.getValue()
 
     const itemRepository = this.itemRepositoryResolver.resolve(roleNames)
 
-    await itemRepository.deleteByUserUuid(event.payload.userUuid)
+    await itemRepository.deleteByUserUuidAndNotInSharedVault(userUuid)
 
     const deletingVaultsResult = await this.deleteSharedVaults.execute({
       ownerUuid: event.payload.userUuid,
@@ -34,6 +44,16 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
       )
     }
 
+    const deletedSharedVaultUuids = Array.from(deletingVaultsResult.getValue().keys())
+
+    this.logger.debug(
+      `Deleting items from shared vaults: ${deletedSharedVaultUuids.map((uuid) => uuid.value).join(', ')}`,
+    )
+
+    if (deletedSharedVaultUuids.length !== 0) {
+      await itemRepository.deleteByUserUuidInSharedVaults(userUuid, deletedSharedVaultUuids)
+    }
+
     const deletingUserFromOtherVaultsResult = await this.removeUserFromSharedVaults.execute({
       userUuid: event.payload.userUuid,
     })

+ 2 - 1
packages/syncing-server/src/Domain/Item/ItemRepositoryInterface.ts

@@ -6,7 +6,8 @@ import { ExtendedIntegrityPayload } from './ExtendedIntegrityPayload'
 import { ItemContentSizeDescriptor } from './ItemContentSizeDescriptor'
 
 export interface ItemRepositoryInterface {
-  deleteByUserUuid(userUuid: string): Promise<void>
+  deleteByUserUuidAndNotInSharedVault(userUuid: Uuid): Promise<void>
+  deleteByUserUuidInSharedVaults(userUuid: Uuid, sharedVaultUuids: Uuid[]): Promise<void>
   findAll(query: ItemQuery): Promise<Item[]>
   countAll(query: ItemQuery): Promise<number>
   findContentSizeForComputingTransferLimit(query: ItemQuery): Promise<Array<ItemContentSizeDescriptor>>

+ 4 - 4
packages/syncing-server/src/Domain/UseCase/SharedVaults/DeleteSharedVault/DeleteSharedVault.ts

@@ -10,7 +10,7 @@ import { CancelInviteToSharedVault } from '../CancelInviteToSharedVault/CancelIn
 import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
 import { TransferSharedVault } from '../TransferSharedVault/TransferSharedVault'
 
-export class DeleteSharedVault implements UseCaseInterface<void> {
+export class DeleteSharedVault implements UseCaseInterface<{ status: 'deleted' | 'transitioned' }> {
   constructor(
     private sharedVaultRepository: SharedVaultRepositoryInterface,
     private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
@@ -22,7 +22,7 @@ export class DeleteSharedVault implements UseCaseInterface<void> {
     private transferSharedVault: TransferSharedVault,
   ) {}
 
-  async execute(dto: DeleteSharedVaultDTO): Promise<Result<void>> {
+  async execute(dto: DeleteSharedVaultDTO): Promise<Result<{ status: 'deleted' | 'transitioned' }>> {
     const originatorUuidOrError = Uuid.create(dto.originatorUuid)
     if (originatorUuidOrError.isFailed()) {
       return Result.fail(originatorUuidOrError.getError())
@@ -79,7 +79,7 @@ export class DeleteSharedVault implements UseCaseInterface<void> {
         return Result.fail(removingOwnerFromSharedVaultResult.getError())
       }
 
-      return Result.ok()
+      return Result.ok({ status: 'transitioned' })
     }
 
     const sharedVaultUsers = await this.sharedVaultUserRepository.findBySharedVaultUuid(sharedVaultUuid)
@@ -105,6 +105,6 @@ export class DeleteSharedVault implements UseCaseInterface<void> {
       }),
     )
 
-    return Result.ok()
+    return Result.ok({ status: 'deleted' })
   }
 }

+ 1 - 1
packages/syncing-server/src/Domain/UseCase/SharedVaults/DeleteSharedVaults/DeleteSharedVaults.spec.ts

@@ -24,7 +24,7 @@ describe('DeleteSharedVaults', () => {
     sharedVaultRepository.findByUserUuid = jest.fn().mockResolvedValue([sharedVault])
 
     deleteSharedVaultUseCase = {} as jest.Mocked<DeleteSharedVault>
-    deleteSharedVaultUseCase.execute = jest.fn().mockResolvedValue(Result.ok())
+    deleteSharedVaultUseCase.execute = jest.fn().mockResolvedValue(Result.ok({ status: 'deleted' }))
   })
 
   it('should delete all shared vaults for a user', async () => {

+ 6 - 3
packages/syncing-server/src/Domain/UseCase/SharedVaults/DeleteSharedVaults/DeleteSharedVaults.ts

@@ -4,13 +4,13 @@ import { DeleteSharedVaultsDTO } from './DeleteSharedVaultsDTO'
 import { DeleteSharedVault } from '../DeleteSharedVault/DeleteSharedVault'
 import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
 
-export class DeleteSharedVaults implements UseCaseInterface<void> {
+export class DeleteSharedVaults implements UseCaseInterface<Map<Uuid, 'deleted' | 'transitioned'>> {
   constructor(
     private sharedVaultRepository: SharedVaultRepositoryInterface,
     private deleteSharedVaultUseCase: DeleteSharedVault,
   ) {}
 
-  async execute(dto: DeleteSharedVaultsDTO): Promise<Result<void>> {
+  async execute(dto: DeleteSharedVaultsDTO): Promise<Result<Map<Uuid, 'deleted' | 'transitioned'>>> {
     const ownerUuidOrError = Uuid.create(dto.ownerUuid)
     if (ownerUuidOrError.isFailed()) {
       return Result.fail(ownerUuidOrError.getError())
@@ -19,6 +19,7 @@ export class DeleteSharedVaults implements UseCaseInterface<void> {
 
     const sharedVaults = await this.sharedVaultRepository.findByUserUuid(ownerUuid)
 
+    const results = new Map<Uuid, 'deleted' | 'transitioned'>()
     for (const sharedVault of sharedVaults) {
       const result = await this.deleteSharedVaultUseCase.execute({
         originatorUuid: ownerUuid.value,
@@ -27,8 +28,10 @@ export class DeleteSharedVaults implements UseCaseInterface<void> {
       if (result.isFailed()) {
         return Result.fail(result.getError())
       }
+
+      results.set(sharedVault.uuid, result.getValue().status)
     }
 
-    return Result.ok()
+    return Result.ok(results)
   }
 }

+ 49 - 1
packages/syncing-server/src/Domain/UseCase/SharedVaults/DesignateSurvivor/DesignateSurvivor.spec.ts

@@ -1,4 +1,11 @@
-import { SharedVaultUser, SharedVaultUserPermission, Timestamps, Uuid } from '@standardnotes/domain-core'
+import {
+  NotificationPayload,
+  Result,
+  SharedVaultUser,
+  SharedVaultUserPermission,
+  Timestamps,
+  Uuid,
+} from '@standardnotes/domain-core'
 
 import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
 import { DesignateSurvivor } from './DesignateSurvivor'
@@ -7,6 +14,7 @@ import { DomainEventInterface, DomainEventPublisherInterface } from '@standardno
 import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
 import { SharedVault } from '../../../SharedVault/SharedVault'
 import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
+import { AddNotificationForUser } from '../../Messaging/AddNotificationForUser/AddNotificationForUser'
 
 describe('DesignateSurvivor', () => {
   let sharedVault: SharedVault
@@ -17,6 +25,7 @@ describe('DesignateSurvivor', () => {
   let timer: TimerInterface
   let domainEventFactory: DomainEventFactoryInterface
   let domainEventPublisher: DomainEventPublisherInterface
+  let addNotificationForUser: AddNotificationForUser
 
   const createUseCase = () =>
     new DesignateSurvivor(
@@ -25,6 +34,7 @@ describe('DesignateSurvivor', () => {
       timer,
       domainEventFactory,
       domainEventPublisher,
+      addNotificationForUser,
     )
 
   beforeEach(() => {
@@ -68,6 +78,9 @@ describe('DesignateSurvivor', () => {
 
     domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
     domainEventPublisher.publish = jest.fn()
+
+    addNotificationForUser = {} as jest.Mocked<AddNotificationForUser>
+    addNotificationForUser.execute = jest.fn().mockReturnValue(Result.ok())
   })
 
   it('should fail if shared vault uuid is invalid', async () => {
@@ -189,4 +202,39 @@ describe('DesignateSurvivor', () => {
     expect(sharedVaultUser.props.isDesignatedSurvivor).toBe(true)
     expect(sharedVaultUserRepository.save).toBeCalledTimes(2)
   })
+
+  it('should fail if it fails to add notification for user', async () => {
+    sharedVaultUserRepository.findBySharedVaultUuid = jest.fn().mockReturnValue([sharedVaultOwner, sharedVaultUser])
+
+    addNotificationForUser.execute = jest.fn().mockReturnValue(Result.fail('Failed to add notification'))
+
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      originatorUuid: '00000000-0000-0000-0000-000000000002',
+    })
+
+    expect(result.isFailed()).toBe(true)
+  })
+
+  it('should fail if it fails to create notification payload', async () => {
+    sharedVaultUserRepository.findBySharedVaultUuid = jest.fn().mockReturnValue([sharedVaultOwner, sharedVaultUser])
+
+    const mock = jest.spyOn(NotificationPayload, 'create')
+    mock.mockReturnValue(Result.fail('Oops'))
+
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      originatorUuid: '00000000-0000-0000-0000-000000000002',
+    })
+
+    expect(result.isFailed()).toBe(true)
+
+    mock.mockRestore()
+  })
 })

+ 38 - 6
packages/syncing-server/src/Domain/UseCase/SharedVaults/DesignateSurvivor/DesignateSurvivor.ts

@@ -1,6 +1,9 @@
 import { TimerInterface } from '@standardnotes/time'
 import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
 import {
+  NotificationPayload,
+  NotificationPayloadIdentifierType,
+  NotificationType,
   Result,
   SharedVaultUser,
   SharedVaultUserPermission,
@@ -13,6 +16,7 @@ import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/Sh
 import { DesignateSurvivorDTO } from './DesignateSurvivorDTO'
 import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
 import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
+import { AddNotificationForUser } from '../../Messaging/AddNotificationForUser/AddNotificationForUser'
 
 export class DesignateSurvivor implements UseCaseInterface<void> {
   constructor(
@@ -21,6 +25,7 @@ export class DesignateSurvivor implements UseCaseInterface<void> {
     private timer: TimerInterface,
     private domainEventFactory: DomainEventFactoryInterface,
     private domainEventPublisher: DomainEventPublisherInterface,
+    private addNotificationForUser: AddNotificationForUser,
   ) {}
 
   async execute(dto: DesignateSurvivorDTO): Promise<Result<void>> {
@@ -91,6 +96,13 @@ export class DesignateSurvivor implements UseCaseInterface<void> {
 
     await this.sharedVaultUserRepository.save(toBeDesignatedAsASurvivor)
 
+    sharedVault.props.timestamps = Timestamps.create(
+      sharedVault.props.timestamps.createdAt,
+      this.timer.getTimestampInMicroseconds(),
+    ).getValue()
+
+    await this.sharedVaultRepository.save(sharedVault)
+
     await this.domainEventPublisher.publish(
       this.domainEventFactory.createUserDesignatedAsSurvivorInSharedVaultEvent({
         sharedVaultUuid: sharedVaultUuid.value,
@@ -99,12 +111,32 @@ export class DesignateSurvivor implements UseCaseInterface<void> {
       }),
     )
 
-    sharedVault.props.timestamps = Timestamps.create(
-      sharedVault.props.timestamps.createdAt,
-      this.timer.getTimestampInMicroseconds(),
-    ).getValue()
-
-    await this.sharedVaultRepository.save(sharedVault)
+    const notificationPayloadOrError = NotificationPayload.create({
+      primaryIdentifier: sharedVault.uuid,
+      primaryIndentifierType: NotificationPayloadIdentifierType.create(
+        NotificationPayloadIdentifierType.TYPES.SharedVaultUuid,
+      ).getValue(),
+      secondaryIdentifier: userUuid,
+      secondaryIdentifierType: NotificationPayloadIdentifierType.create(
+        NotificationPayloadIdentifierType.TYPES.UserUuid,
+      ).getValue(),
+      type: NotificationType.create(NotificationType.TYPES.UserDesignatedAsSurvivor).getValue(),
+      version: '1.0',
+    })
+    if (notificationPayloadOrError.isFailed()) {
+      return Result.fail(notificationPayloadOrError.getError())
+    }
+    const notificationPayload = notificationPayloadOrError.getValue()
+
+    const result = await this.addNotificationForUser.execute({
+      userUuid: sharedVault.props.userUuid.value,
+      type: NotificationType.TYPES.UserDesignatedAsSurvivor,
+      payload: notificationPayload,
+      version: '1.0',
+    })
+    if (result.isFailed()) {
+      return Result.fail(result.getError())
+    }
 
     return Result.ok()
   }

+ 1 - 1
packages/syncing-server/src/Domain/UseCase/Transition/TransitionItemsFromPrimaryToSecondaryDatabaseForUser/TransitionItemsFromPrimaryToSecondaryDatabaseForUser.ts

@@ -153,7 +153,7 @@ export class TransitionItemsFromPrimaryToSecondaryDatabaseForUser implements Use
     try {
       this.logger.info(`[${userUuid.value}] Cleaning up primary database items`)
 
-      await itemRepository.deleteByUserUuid(userUuid.value)
+      await itemRepository.deleteByUserUuidAndNotInSharedVault(userUuid)
 
       return Result.ok()
     } catch (error) {

+ 13 - 2
packages/syncing-server/src/Infra/TypeORM/MongoDBItemRepository.ts

@@ -17,6 +17,15 @@ export class MongoDBItemRepository implements ItemRepositoryInterface {
     private logger: Logger,
   ) {}
 
+  async deleteByUserUuidInSharedVaults(userUuid: Uuid, sharedVaultUuids: Uuid[]): Promise<void> {
+    await this.mongoRepository.deleteMany({
+      $and: [
+        { userUuid: { $eq: userUuid.value } },
+        { sharedVaultUuid: { $in: sharedVaultUuids.map((uuid) => uuid.value) } },
+      ],
+    })
+  }
+
   async updateSharedVaultOwner(dto: { sharedVaultUuid: Uuid; fromOwnerUuid: Uuid; toOwnerUuid: Uuid }): Promise<void> {
     await this.mongoRepository.updateMany(
       {
@@ -37,8 +46,10 @@ export class MongoDBItemRepository implements ItemRepositoryInterface {
     await this.mongoRepository.deleteOne({ _id: { $eq: BSON.UUID.createFromHexString(uuid.value) } })
   }
 
-  async deleteByUserUuid(userUuid: string): Promise<void> {
-    await this.mongoRepository.deleteMany({ userUuid })
+  async deleteByUserUuidAndNotInSharedVault(userUuid: Uuid): Promise<void> {
+    await this.mongoRepository.deleteMany({
+      $and: [{ userUuid: { $eq: userUuid.value } }, { sharedVaultUuid: { $eq: null } }],
+    })
   }
 
   async findAll(query: ItemQuery): Promise<Item[]> {

+ 22 - 0
packages/syncing-server/src/Infra/TypeORM/SQLItemRepository.ts

@@ -16,6 +16,28 @@ export class SQLItemRepository extends SQLLegacyItemRepository {
     super(ormRepository, mapper, logger)
   }
 
+  override async deleteByUserUuidInSharedVaults(userUuid: Uuid, sharedVaultUuids: Uuid[]): Promise<void> {
+    await this.ormRepository
+      .createQueryBuilder('item')
+      .delete()
+      .from('items')
+      .where('user_uuid = :userUuid', { userUuid: userUuid.value })
+      .andWhere('shared_vault_uuid IN (:...sharedVaultUuids)', {
+        sharedVaultUuids: sharedVaultUuids.map((uuid) => uuid.value),
+      })
+      .execute()
+  }
+
+  override async deleteByUserUuidAndNotInSharedVault(userUuid: Uuid): Promise<void> {
+    await this.ormRepository
+      .createQueryBuilder('item')
+      .delete()
+      .from('items')
+      .where('user_uuid = :userUuid', { userUuid: userUuid.value })
+      .andWhere('shared_vault_uuid IS NULL')
+      .execute()
+  }
+
   override async updateSharedVaultOwner(dto: {
     sharedVaultUuid: Uuid
     fromOwnerUuid: Uuid

+ 6 - 2
packages/syncing-server/src/Infra/TypeORM/SQLLegacyItemRepository.ts

@@ -16,6 +16,10 @@ export class SQLLegacyItemRepository implements ItemRepositoryInterface {
     protected logger: Logger,
   ) {}
 
+  async deleteByUserUuidInSharedVaults(_userUuid: Uuid, _sharedVaultUuids: Uuid[]): Promise<void> {
+    this.logger.error('Method deleteByUserUuidInSharedVaults not supported.')
+  }
+
   async updateSharedVaultOwner(_dto: { sharedVaultUuid: Uuid; fromOwnerUuid: Uuid; toOwnerUuid: Uuid }): Promise<void> {
     this.logger.error('Method updateSharedVaultOwner not supported.')
   }
@@ -80,12 +84,12 @@ export class SQLLegacyItemRepository implements ItemRepositoryInterface {
     return itemContentSizeDescriptors
   }
 
-  async deleteByUserUuid(userUuid: string): Promise<void> {
+  async deleteByUserUuidAndNotInSharedVault(userUuid: Uuid): Promise<void> {
     await this.ormRepository
       .createQueryBuilder('item')
       .delete()
       .from('items')
-      .where('user_uuid = :userUuid', { userUuid })
+      .where('user_uuid = :userUuid', { userUuid: userUuid.value })
       .execute()
   }