Browse Source

feat(syncing-server): transfer shared vault ownership to designated survivor upon account deletion (#845)

Karol Sójko 1 năm trước cách đây
mục cha
commit
0a1080ce2a

+ 32 - 21
packages/syncing-server/src/Bootstrap/Container.ts

@@ -170,6 +170,7 @@ import { RemoveItemsFromSharedVault } from '../Domain/UseCase/SharedVaults/Remov
 import { SharedVaultRemovedEventHandler } from '../Domain/Handler/SharedVaultRemovedEventHandler'
 import { DesignateSurvivor } from '../Domain/UseCase/SharedVaults/DesignateSurvivor/DesignateSurvivor'
 import { RemoveUserFromSharedVaults } from '../Domain/UseCase/SharedVaults/RemoveUserFromSharedVaults/RemoveUserFromSharedVaults'
+import { TransferSharedVault } from '../Domain/UseCase/SharedVaults/TransferSharedVault/TransferSharedVault'
 
 export class ContainerConfigLoader {
   private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
@@ -782,27 +783,6 @@ export class ContainerConfigLoader {
           container.get(TYPES.Sync_Timer),
         ),
       )
-    container
-      .bind<DeleteSharedVault>(TYPES.Sync_DeleteSharedVault)
-      .toConstantValue(
-        new DeleteSharedVault(
-          container.get<SharedVaultRepositoryInterface>(TYPES.Sync_SharedVaultRepository),
-          container.get<SharedVaultUserRepositoryInterface>(TYPES.Sync_SharedVaultUserRepository),
-          container.get<SharedVaultInviteRepositoryInterface>(TYPES.Sync_SharedVaultInviteRepository),
-          container.get<RemoveUserFromSharedVault>(TYPES.Sync_RemoveSharedVaultUser),
-          container.get<DeclineInviteToSharedVault>(TYPES.Sync_DeclineInviteToSharedVault),
-          container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
-          container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
-        ),
-      )
-    container
-      .bind<DeleteSharedVaults>(TYPES.Sync_DeleteSharedVaults)
-      .toConstantValue(
-        new DeleteSharedVaults(
-          container.get<SharedVaultRepositoryInterface>(TYPES.Sync_SharedVaultRepository),
-          container.get<DeleteSharedVault>(TYPES.Sync_DeleteSharedVault),
-        ),
-      )
     container
       .bind<CreateSharedVaultFileValetToken>(TYPES.Sync_CreateSharedVaultFileValetToken)
       .toConstantValue(
@@ -887,6 +867,37 @@ export class ContainerConfigLoader {
           container.get<Logger>(TYPES.Sync_Logger),
         ),
       )
+    container
+      .bind<TransferSharedVault>(TYPES.Sync_TransferSharedVault)
+      .toConstantValue(
+        new TransferSharedVault(
+          container.get<SharedVaultRepositoryInterface>(TYPES.Sync_SharedVaultRepository),
+          container.get<SharedVaultUserRepositoryInterface>(TYPES.Sync_SharedVaultUserRepository),
+          container.get<TimerInterface>(TYPES.Sync_Timer),
+        ),
+      )
+    container
+      .bind<DeleteSharedVault>(TYPES.Sync_DeleteSharedVault)
+      .toConstantValue(
+        new DeleteSharedVault(
+          container.get<SharedVaultRepositoryInterface>(TYPES.Sync_SharedVaultRepository),
+          container.get<SharedVaultUserRepositoryInterface>(TYPES.Sync_SharedVaultUserRepository),
+          container.get<SharedVaultInviteRepositoryInterface>(TYPES.Sync_SharedVaultInviteRepository),
+          container.get<RemoveUserFromSharedVault>(TYPES.Sync_RemoveSharedVaultUser),
+          container.get<DeclineInviteToSharedVault>(TYPES.Sync_DeclineInviteToSharedVault),
+          container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
+          container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
+          container.get<TransferSharedVault>(TYPES.Sync_TransferSharedVault),
+        ),
+      )
+    container
+      .bind<DeleteSharedVaults>(TYPES.Sync_DeleteSharedVaults)
+      .toConstantValue(
+        new DeleteSharedVaults(
+          container.get<SharedVaultRepositoryInterface>(TYPES.Sync_SharedVaultRepository),
+          container.get<DeleteSharedVault>(TYPES.Sync_DeleteSharedVault),
+        ),
+      )
 
     // Services
     container

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

@@ -89,6 +89,7 @@ const TYPES = {
   Sync_RemoveItemsFromSharedVault: Symbol.for('Sync_RemoveItemsFromSharedVault'),
   Sync_DesignateSurvivor: Symbol.for('Sync_DesignateSurvivor'),
   Sync_RemoveUserFromSharedVaults: Symbol.for('Sync_RemoveUserFromSharedVaults'),
+  Sync_TransferSharedVault: Symbol.for('Sync_TransferSharedVault'),
   // Handlers
   Sync_AccountDeletionRequestedEventHandler: Symbol.for('Sync_AccountDeletionRequestedEventHandler'),
   Sync_DuplicateItemSyncedEventHandler: Symbol.for('Sync_DuplicateItemSyncedEventHandler'),

+ 61 - 2
packages/syncing-server/src/Domain/UseCase/SharedVaults/DeleteSharedVault/DeleteSharedVault.spec.ts

@@ -10,6 +10,7 @@ import { RemoveUserFromSharedVault } from '../RemoveUserFromSharedVault/RemoveUs
 import { DeclineInviteToSharedVault } from '../DeclineInviteToSharedVault/DeclineInviteToSharedVault'
 import { SharedVaultInvite } from '../../../SharedVault/User/Invite/SharedVaultInvite'
 import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
+import { TransferSharedVault } from '../TransferSharedVault/TransferSharedVault'
 
 describe('DeleteSharedVault', () => {
   let sharedVaultRepository: SharedVaultRepositoryInterface
@@ -22,6 +23,7 @@ describe('DeleteSharedVault', () => {
   let sharedVaultInvite: SharedVaultInvite
   let domainEventFactory: DomainEventFactoryInterface
   let domainEventPublisher: DomainEventPublisherInterface
+  let transferSharedVault: TransferSharedVault
 
   const createUseCase = () =>
     new DeleteSharedVault(
@@ -32,9 +34,13 @@ describe('DeleteSharedVault', () => {
       declineInviteToSharedVault,
       domainEventFactory,
       domainEventPublisher,
+      transferSharedVault,
     )
 
   beforeEach(() => {
+    transferSharedVault = {} as jest.Mocked<TransferSharedVault>
+    transferSharedVault.execute = jest.fn().mockReturnValue(Result.ok())
+
     sharedVault = SharedVault.create({
       fileUploadBytesUsed: 2,
       userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
@@ -53,6 +59,7 @@ describe('DeleteSharedVault', () => {
     }).getValue()
     sharedVaultUserRepository = {} as jest.Mocked<SharedVaultUserRepositoryInterface>
     sharedVaultUserRepository.findBySharedVaultUuid = jest.fn().mockResolvedValue([sharedVaultUser])
+    sharedVaultUserRepository.findDesignatedSurvivorBySharedVaultUuid = jest.fn().mockResolvedValue(null)
 
     sharedVaultInvite = SharedVaultInvite.create({
       encryptedMessage: 'test',
@@ -171,7 +178,6 @@ describe('DeleteSharedVault', () => {
 
     expect(result.isFailed()).toBeTruthy()
     expect(sharedVaultRepository.remove).not.toHaveBeenCalled()
-    expect(declineInviteToSharedVault.execute).not.toHaveBeenCalled()
     expect(removeUserFromSharedVault.execute).toHaveBeenCalled()
   })
 
@@ -187,6 +193,59 @@ describe('DeleteSharedVault', () => {
     expect(result.isFailed()).toBeTruthy()
     expect(sharedVaultRepository.remove).not.toHaveBeenCalled()
     expect(declineInviteToSharedVault.execute).toHaveBeenCalled()
-    expect(removeUserFromSharedVault.execute).toHaveBeenCalled()
+    expect(removeUserFromSharedVault.execute).not.toHaveBeenCalled()
+  })
+
+  describe('when shared vault has designated survivor', () => {
+    beforeEach(() => {
+      sharedVaultUserRepository.findDesignatedSurvivorBySharedVaultUuid = jest.fn().mockResolvedValue(sharedVaultUser)
+    })
+
+    it('should transfer shared vault to designated survivor', async () => {
+      const useCase = createUseCase()
+
+      const result = await useCase.execute({
+        sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+        originatorUuid: '00000000-0000-0000-0000-000000000000',
+      })
+
+      expect(result.isFailed()).toBeFalsy()
+      expect(sharedVaultRepository.remove).not.toHaveBeenCalled()
+      expect(declineInviteToSharedVault.execute).toHaveBeenCalled()
+      expect(removeUserFromSharedVault.execute).toHaveBeenCalled()
+      expect(transferSharedVault.execute).toHaveBeenCalled()
+    })
+
+    it('should fail if transfering shared vault to designated survivor fails', async () => {
+      transferSharedVault.execute = jest.fn().mockReturnValue(Result.fail('failed'))
+      const useCase = createUseCase()
+
+      const result = await useCase.execute({
+        sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+        originatorUuid: '00000000-0000-0000-0000-000000000000',
+      })
+
+      expect(result.isFailed()).toBeTruthy()
+      expect(sharedVaultRepository.remove).not.toHaveBeenCalled()
+      expect(declineInviteToSharedVault.execute).toHaveBeenCalled()
+      expect(removeUserFromSharedVault.execute).not.toHaveBeenCalled()
+      expect(transferSharedVault.execute).toHaveBeenCalled()
+    })
+
+    it('should fail if removing owner from shared vault fails', async () => {
+      removeUserFromSharedVault.execute = jest.fn().mockReturnValue(Result.fail('failed'))
+      const useCase = createUseCase()
+
+      const result = await useCase.execute({
+        sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+        originatorUuid: '00000000-0000-0000-0000-000000000000',
+      })
+
+      expect(result.isFailed()).toBeTruthy()
+      expect(sharedVaultRepository.remove).not.toHaveBeenCalled()
+      expect(declineInviteToSharedVault.execute).toHaveBeenCalled()
+      expect(removeUserFromSharedVault.execute).toHaveBeenCalled()
+      expect(transferSharedVault.execute).toHaveBeenCalled()
+    })
   })
 })

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

@@ -8,6 +8,7 @@ import { SharedVaultInviteRepositoryInterface } from '../../../SharedVault/User/
 import { RemoveUserFromSharedVault } from '../RemoveUserFromSharedVault/RemoveUserFromSharedVault'
 import { DeclineInviteToSharedVault } from '../DeclineInviteToSharedVault/DeclineInviteToSharedVault'
 import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
+import { TransferSharedVault } from '../TransferSharedVault/TransferSharedVault'
 
 export class DeleteSharedVault implements UseCaseInterface<void> {
   constructor(
@@ -18,6 +19,7 @@ export class DeleteSharedVault implements UseCaseInterface<void> {
     private declineInviteToSharedVault: DeclineInviteToSharedVault,
     private domainEventFactory: DomainEventFactoryInterface,
     private domainEventPublisher: DomainEventPublisherInterface,
+    private transferSharedVault: TransferSharedVault,
   ) {}
 
   async execute(dto: DeleteSharedVaultDTO): Promise<Result<void>> {
@@ -42,13 +44,11 @@ export class DeleteSharedVault implements UseCaseInterface<void> {
       return Result.fail('Shared vault does not belong to the user')
     }
 
-    const sharedVaultUsers = await this.sharedVaultUserRepository.findBySharedVaultUuid(sharedVaultUuid)
-    for (const sharedVaultUser of sharedVaultUsers) {
-      const result = await this.removeUserFromSharedVault.execute({
-        originatorUuid: originatorUuid.value,
-        sharedVaultUuid: sharedVaultUuid.value,
-        userUuid: sharedVaultUser.props.userUuid.value,
-        forceRemoveOwner: true,
+    const sharedVaultInvites = await this.sharedVaultInviteRepository.findBySharedVaultUuid(sharedVaultUuid)
+    for (const sharedVaultInvite of sharedVaultInvites) {
+      const result = await this.declineInviteToSharedVault.execute({
+        inviteUuid: sharedVaultInvite.id.toString(),
+        userUuid: sharedVaultInvite.props.userUuid.value,
       })
 
       if (result.isFailed()) {
@@ -56,11 +56,39 @@ export class DeleteSharedVault implements UseCaseInterface<void> {
       }
     }
 
-    const sharedVaultInvites = await this.sharedVaultInviteRepository.findBySharedVaultUuid(sharedVaultUuid)
-    for (const sharedVaultInvite of sharedVaultInvites) {
-      const result = await this.declineInviteToSharedVault.execute({
-        inviteUuid: sharedVaultInvite.id.toString(),
-        userUuid: sharedVaultInvite.props.userUuid.value,
+    const sharedVaultDesignatedSurvivor =
+      await this.sharedVaultUserRepository.findDesignatedSurvivorBySharedVaultUuid(sharedVaultUuid)
+    if (sharedVaultDesignatedSurvivor) {
+      const result = await this.transferSharedVault.execute({
+        sharedVaultUid: sharedVaultUuid.value,
+        fromUserUuid: originatorUuid.value,
+        toUserUuid: sharedVaultDesignatedSurvivor.props.userUuid.value,
+      })
+
+      if (result.isFailed()) {
+        return Result.fail(result.getError())
+      }
+
+      const removingOwnerFromSharedVaultResult = await this.removeUserFromSharedVault.execute({
+        originatorUuid: originatorUuid.value,
+        sharedVaultUuid: sharedVaultUuid.value,
+        userUuid: originatorUuid.value,
+        forceRemoveOwner: true,
+      })
+      if (removingOwnerFromSharedVaultResult.isFailed()) {
+        return Result.fail(removingOwnerFromSharedVaultResult.getError())
+      }
+
+      return Result.ok()
+    }
+
+    const sharedVaultUsers = await this.sharedVaultUserRepository.findBySharedVaultUuid(sharedVaultUuid)
+    for (const sharedVaultUser of sharedVaultUsers) {
+      const result = await this.removeUserFromSharedVault.execute({
+        originatorUuid: originatorUuid.value,
+        sharedVaultUuid: sharedVaultUuid.value,
+        userUuid: sharedVaultUser.props.userUuid.value,
+        forceRemoveOwner: true,
       })
 
       if (result.isFailed()) {

+ 148 - 0
packages/syncing-server/src/Domain/UseCase/SharedVaults/TransferSharedVault/TransferSharedVault.spec.ts

@@ -0,0 +1,148 @@
+import { TimerInterface } from '@standardnotes/time'
+import { SharedVaultUser, SharedVaultUserPermission, Timestamps, Uuid } from '@standardnotes/domain-core'
+
+import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
+import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
+import { TransferSharedVault } from './TransferSharedVault'
+import { SharedVault } from '../../../SharedVault/SharedVault'
+
+describe('TransferSharedVault', () => {
+  let sharedVault: SharedVault
+  let sharedVaultUser: SharedVaultUser
+  let sharedVaultRepository: SharedVaultRepositoryInterface
+  let sharedVaultUserRepository: SharedVaultUserRepositoryInterface
+  let timer: TimerInterface
+
+  const createUseCase = () => new TransferSharedVault(sharedVaultRepository, sharedVaultUserRepository, timer)
+
+  beforeEach(() => {
+    sharedVault = SharedVault.create({
+      fileUploadBytesUsed: 2,
+      userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+      timestamps: Timestamps.create(123, 123).getValue(),
+    }).getValue()
+
+    sharedVaultUser = SharedVaultUser.create({
+      permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Read).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()
+
+    sharedVaultRepository = {} as jest.Mocked<SharedVaultRepositoryInterface>
+    sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(sharedVault)
+    sharedVaultRepository.save = jest.fn()
+
+    sharedVaultUserRepository = {} as jest.Mocked<SharedVaultUserRepositoryInterface>
+    sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(sharedVaultUser)
+    sharedVaultUserRepository.save = jest.fn()
+
+    timer = {} as jest.Mocked<TimerInterface>
+    timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123)
+  })
+
+  it('should transfer shared vault to another user', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      sharedVaultUid: '00000000-0000-0000-0000-000000000000',
+      fromUserUuid: '00000000-0000-0000-0000-000000000000',
+      toUserUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBe(false)
+    expect(sharedVaultRepository.save).toHaveBeenCalled()
+    expect(sharedVaultUserRepository.save).toHaveBeenCalled()
+  })
+
+  it('should fail if shared vault does not exist', async () => {
+    const useCase = createUseCase()
+
+    sharedVaultRepository.findByUuid = jest.fn().mockResolvedValue(null)
+
+    const result = await useCase.execute({
+      sharedVaultUid: '00000000-0000-0000-0000-000000000000',
+      fromUserUuid: '00000000-0000-0000-0000-000000000000',
+      toUserUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBe(true)
+    expect(sharedVaultRepository.save).not.toHaveBeenCalled()
+    expect(sharedVaultUserRepository.save).not.toHaveBeenCalled()
+  })
+
+  it('should fail if shared vault does not belong to user', async () => {
+    const useCase = createUseCase()
+
+    sharedVault.props.userUuid = Uuid.create('00000000-0000-0000-0000-000000000001').getValue()
+
+    const result = await useCase.execute({
+      sharedVaultUid: '00000000-0000-0000-0000-000000000000',
+      fromUserUuid: '00000000-0000-0000-0000-000000000000',
+      toUserUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBe(true)
+    expect(sharedVaultRepository.save).not.toHaveBeenCalled()
+    expect(sharedVaultUserRepository.save).not.toHaveBeenCalled()
+  })
+
+  it('should fail if new owner is not a member of shared vault', async () => {
+    const useCase = createUseCase()
+
+    sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockResolvedValue(null)
+
+    const result = await useCase.execute({
+      sharedVaultUid: '00000000-0000-0000-0000-000000000000',
+      fromUserUuid: '00000000-0000-0000-0000-000000000000',
+      toUserUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBe(true)
+    expect(sharedVaultRepository.save).not.toHaveBeenCalled()
+    expect(sharedVaultUserRepository.save).not.toHaveBeenCalled()
+  })
+
+  it('should fail if shared vault uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      sharedVaultUid: 'invalid',
+      fromUserUuid: '00000000-0000-0000-0000-000000000000',
+      toUserUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBe(true)
+    expect(sharedVaultRepository.save).not.toHaveBeenCalled()
+    expect(sharedVaultUserRepository.save).not.toHaveBeenCalled()
+  })
+
+  it('should fail if from user uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      sharedVaultUid: '00000000-0000-0000-0000-000000000000',
+      fromUserUuid: 'invalid',
+      toUserUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBe(true)
+    expect(sharedVaultRepository.save).not.toHaveBeenCalled()
+    expect(sharedVaultUserRepository.save).not.toHaveBeenCalled()
+  })
+
+  it('should fail if to user uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      sharedVaultUid: '00000000-0000-0000-0000-000000000000',
+      fromUserUuid: '00000000-0000-0000-0000-000000000000',
+      toUserUuid: 'invalid',
+    })
+
+    expect(result.isFailed()).toBe(true)
+    expect(sharedVaultRepository.save).not.toHaveBeenCalled()
+    expect(sharedVaultUserRepository.save).not.toHaveBeenCalled()
+  })
+})

+ 70 - 0
packages/syncing-server/src/Domain/UseCase/SharedVaults/TransferSharedVault/TransferSharedVault.ts

@@ -0,0 +1,70 @@
+import { Result, SharedVaultUserPermission, Timestamps, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
+import { TimerInterface } from '@standardnotes/time'
+
+import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
+import { TransferSharedVaultDTO } from './TransferSharedVaultDTO'
+import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
+
+export class TransferSharedVault implements UseCaseInterface<void> {
+  constructor(
+    private sharedVaultRepository: SharedVaultRepositoryInterface,
+    private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
+    private timer: TimerInterface,
+  ) {}
+
+  async execute(dto: TransferSharedVaultDTO): Promise<Result<void>> {
+    const sharedVaultUuidOrError = Uuid.create(dto.sharedVaultUid)
+    if (sharedVaultUuidOrError.isFailed()) {
+      return Result.fail(sharedVaultUuidOrError.getError())
+    }
+    const sharedVaultUuid = sharedVaultUuidOrError.getValue()
+
+    const fromUserUuidOrError = Uuid.create(dto.fromUserUuid)
+    if (fromUserUuidOrError.isFailed()) {
+      return Result.fail(fromUserUuidOrError.getError())
+    }
+    const fromUserUuid = fromUserUuidOrError.getValue()
+
+    const toUserUuidOrError = Uuid.create(dto.toUserUuid)
+    if (toUserUuidOrError.isFailed()) {
+      return Result.fail(toUserUuidOrError.getError())
+    }
+    const toUserUuid = toUserUuidOrError.getValue()
+
+    const sharedVault = await this.sharedVaultRepository.findByUuid(sharedVaultUuid)
+    if (!sharedVault) {
+      return Result.fail('Shared vault not found')
+    }
+
+    if (!sharedVault.props.userUuid.equals(fromUserUuid)) {
+      return Result.fail('Shared vault does not belong to this user')
+    }
+
+    const newOwner = await this.sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid({
+      userUuid: toUserUuid,
+      sharedVaultUuid: sharedVaultUuid,
+    })
+    if (!newOwner) {
+      return Result.fail('New owner is not a member of this shared vault')
+    }
+
+    newOwner.props.isDesignatedSurvivor = false
+    newOwner.props.permission = SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Admin).getValue()
+    newOwner.props.timestamps = Timestamps.create(
+      newOwner.props.timestamps.createdAt,
+      this.timer.getTimestampInMicroseconds(),
+    ).getValue()
+
+    await this.sharedVaultUserRepository.save(newOwner)
+
+    sharedVault.props.userUuid = toUserUuid
+    sharedVault.props.timestamps = Timestamps.create(
+      sharedVault.props.timestamps.createdAt,
+      this.timer.getTimestampInMicroseconds(),
+    ).getValue()
+
+    await this.sharedVaultRepository.save(sharedVault)
+
+    return Result.ok()
+  }
+}

+ 5 - 0
packages/syncing-server/src/Domain/UseCase/SharedVaults/TransferSharedVault/TransferSharedVaultDTO.ts

@@ -0,0 +1,5 @@
+export interface TransferSharedVaultDTO {
+  sharedVaultUid: string
+  fromUserUuid: string
+  toUserUuid: string
+}