Browse Source

feat: shared vault invites controller and use cases (#647)

* feat: get shared vault invites sent by user.

Co-authored-by: Mo <mo@standardnotes.com>

* feat: shared vault invites controller.

Co-authored-by: Mo <mo@standardnotes.com>

---------

Co-authored-by: Mo <mo@standardnotes.com>
Karol Sójko 1 năm trước cách đây
mục cha
commit
72310130d2
17 tập tin đã thay đổi với 782 bổ sung3 xóa
  1. 9 0
      packages/syncing-server/src/Bootstrap/Types.ts
  2. 2 0
      packages/syncing-server/src/Domain/SharedVault/User/Invite/SharedVaultInviteRepositoryInterface.ts
  3. 79 0
      packages/syncing-server/src/Domain/UseCase/DeleteSharedVaultInvitesSentByUser/DeleteSharedVaultInvitesSentByUser.spec.ts
  4. 41 0
      packages/syncing-server/src/Domain/UseCase/DeleteSharedVaultInvitesSentByUser/DeleteSharedVaultInvitesSentByUser.ts
  5. 4 0
      packages/syncing-server/src/Domain/UseCase/DeleteSharedVaultInvitesSentByUser/DeleteSharedVaultInvitesSentByUserDTO.ts
  6. 83 0
      packages/syncing-server/src/Domain/UseCase/GetSharedVaultInvitesSentByUser/GetSharedVaultInvitesSentByUser.spec.ts
  7. 36 0
      packages/syncing-server/src/Domain/UseCase/GetSharedVaultInvitesSentByUser/GetSharedVaultInvitesSentByUser.ts
  8. 4 0
      packages/syncing-server/src/Domain/UseCase/GetSharedVaultInvitesSentByUser/GetSharedVaultInvitesSentByUserDTO.ts
  9. 59 0
      packages/syncing-server/src/Domain/UseCase/GetSharedVaultInvitesSentToUser/GetSharedVaultInvitesSentToUser.spec.ts
  10. 18 0
      packages/syncing-server/src/Domain/UseCase/GetSharedVaultInvitesSentToUser/GetSharedVaultInvitesSentToUser.ts
  11. 3 0
      packages/syncing-server/src/Domain/UseCase/GetSharedVaultInvitesSentToUser/GetSharedVaultInvitesSentToUserDTO.ts
  12. 4 3
      packages/syncing-server/src/Domain/UseCase/UpdateSharedVaultInvite/UpdateSharedVaultInvite.ts
  13. 278 0
      packages/syncing-server/src/Infra/InversifyExpressUtils/HomeServer/HomeServerSharedVaultInvitesController.ts
  14. 99 0
      packages/syncing-server/src/Infra/InversifyExpressUtils/InversifyExpressSharedVaultInvitesController.ts
  15. 28 0
      packages/syncing-server/src/Infra/TypeORM/TypeORMSharedVaultInviteRepository.ts
  16. 25 0
      packages/syncing-server/src/Mapping/Http/SharedVaultInviteHttpMapper.ts
  17. 10 0
      packages/syncing-server/src/Mapping/Http/SharedVaultInviteHttpRepresentation.ts

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

@@ -44,6 +44,15 @@ const TYPES = {
   Sync_CreateSharedVaultFileValetToken: Symbol.for('Sync_CreateSharedVaultFileValetToken'),
   Sync_GetSharedVaultUsers: Symbol.for('Sync_GetSharedVaultUsers'),
   Sync_RemoveSharedVaultUser: Symbol.for('Sync_RemoveSharedVaultUser'),
+  Sync_InviteUserToSharedVault: Symbol.for('Sync_InviteUserToSharedVault'),
+  Sync_UpdateSharedVaultInvite: Symbol.for('Sync_UpdateSharedVaultInvite'),
+  Sync_AcceptInviteToSharedVault: Symbol.for('Sync_AcceptInviteToSharedVault'),
+  Sync_DeclineInviteToSharedVault: Symbol.for('Sync_DeclineInviteToSharedVault'),
+  Sync_DeleteSharedVaultInvitesToUser: Symbol.for('Sync_DeleteSharedVaultInvitesToUser'),
+  Sync_DeleteSharedVaultInvitesSentByUser: Symbol.for('Sync_DeleteSharedVaultInvitesSentByUser'),
+  Sync_GetSharedVaultInvitesSentByUser: Symbol.for('Sync_GetSharedVaultInvitesSentByUser'),
+  Sync_GetSharedVaultInvitesSentToUser: Symbol.for('Sync_GetSharedVaultInvitesSentToUser'),
+  Sync_SharedVaultInviteHttpMapper: Symbol.for('Sync_SharedVaultInviteHttpMapper'),
   // Handlers
   Sync_AccountDeletionRequestedEventHandler: Symbol.for('Sync_AccountDeletionRequestedEventHandler'),
   Sync_DuplicateItemSyncedEventHandler: Symbol.for('Sync_DuplicateItemSyncedEventHandler'),

+ 2 - 0
packages/syncing-server/src/Domain/SharedVault/User/Invite/SharedVaultInviteRepositoryInterface.ts

@@ -8,5 +8,7 @@ export interface SharedVaultInviteRepositoryInterface {
   remove(sharedVaultInvite: SharedVaultInvite): Promise<void>
   removeBySharedVaultUuid(sharedVaultUuid: Uuid): Promise<void>
   findByUserUuid(userUuid: Uuid): Promise<SharedVaultInvite[]>
+  findBySenderUuid(senderUuid: Uuid): Promise<SharedVaultInvite[]>
   findByUserUuidAndSharedVaultUuid(dto: { userUuid: Uuid; sharedVaultUuid: Uuid }): Promise<SharedVaultInvite | null>
+  findBySenderUuidAndSharedVaultUuid(dto: { senderUuid: Uuid; sharedVaultUuid: Uuid }): Promise<SharedVaultInvite[]>
 }

+ 79 - 0
packages/syncing-server/src/Domain/UseCase/DeleteSharedVaultInvitesSentByUser/DeleteSharedVaultInvitesSentByUser.spec.ts

@@ -0,0 +1,79 @@
+import { Result, Timestamps, Uuid } from '@standardnotes/domain-core'
+import { SharedVaultInviteRepositoryInterface } from '../../SharedVault/User/Invite/SharedVaultInviteRepositoryInterface'
+import { DeclineInviteToSharedVault } from '../DeclineInviteToSharedVault/DeclineInviteToSharedVault'
+import { DeleteSharedVaultInvitesSentByUser } from './DeleteSharedVaultInvitesSentByUser'
+import { SharedVaultInvite } from '../../SharedVault/User/Invite/SharedVaultInvite'
+import { SharedVaultUserPermission } from '../../SharedVault/User/SharedVaultUserPermission'
+
+describe('DeleteSharedVaultInvitesSentByUser', () => {
+  let sharedVaultInviteRepository: SharedVaultInviteRepositoryInterface
+  let declineInviteToSharedVault: DeclineInviteToSharedVault
+  let sharedVaultInvite: SharedVaultInvite
+
+  const createUseCase = () =>
+    new DeleteSharedVaultInvitesSentByUser(sharedVaultInviteRepository, declineInviteToSharedVault)
+
+  beforeEach(() => {
+    sharedVaultInvite = SharedVaultInvite.create({
+      sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+      userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+      senderUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+      encryptedMessage: 'encrypted-message',
+      permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Read).getValue(),
+      timestamps: Timestamps.create(123, 123).getValue(),
+    }).getValue()
+
+    sharedVaultInviteRepository = {} as jest.Mocked<SharedVaultInviteRepositoryInterface>
+    sharedVaultInviteRepository.findBySenderUuidAndSharedVaultUuid = jest.fn().mockReturnValue([sharedVaultInvite])
+
+    declineInviteToSharedVault = {} as jest.Mocked<DeclineInviteToSharedVault>
+    declineInviteToSharedVault.execute = jest.fn().mockReturnValue(Result.ok())
+  })
+
+  it('should decline all invites by user', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBeFalsy()
+    expect(declineInviteToSharedVault.execute).toHaveBeenCalled()
+  })
+
+  it('should return error when user uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: 'invalid-uuid',
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+  })
+
+  it('should return error when shared vault uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      sharedVaultUuid: 'invalid-uuid',
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+  })
+
+  it('should return error when declineInviteToSharedVault fails', async () => {
+    declineInviteToSharedVault.execute = jest.fn().mockReturnValue(Result.fail('error'))
+
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+  })
+})

+ 41 - 0
packages/syncing-server/src/Domain/UseCase/DeleteSharedVaultInvitesSentByUser/DeleteSharedVaultInvitesSentByUser.ts

@@ -0,0 +1,41 @@
+import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
+import { DeleteSharedVaultInvitesSentByUserDTO } from './DeleteSharedVaultInvitesSentByUserDTO'
+import { DeclineInviteToSharedVault } from '../DeclineInviteToSharedVault/DeclineInviteToSharedVault'
+import { SharedVaultInviteRepositoryInterface } from '../../SharedVault/User/Invite/SharedVaultInviteRepositoryInterface'
+
+export class DeleteSharedVaultInvitesSentByUser implements UseCaseInterface<void> {
+  constructor(
+    private sharedVaultInviteRepository: SharedVaultInviteRepositoryInterface,
+    private declineInviteToSharedVault: DeclineInviteToSharedVault,
+  ) {}
+
+  async execute(dto: DeleteSharedVaultInvitesSentByUserDTO): Promise<Result<void>> {
+    const userUuidOrError = Uuid.create(dto.userUuid)
+    if (userUuidOrError.isFailed()) {
+      return Result.fail(userUuidOrError.getError())
+    }
+    const userUuid = userUuidOrError.getValue()
+
+    const sharedVaultUuidOrError = Uuid.create(dto.sharedVaultUuid)
+    if (sharedVaultUuidOrError.isFailed()) {
+      return Result.fail(sharedVaultUuidOrError.getError())
+    }
+    const sharedVaultUuid = sharedVaultUuidOrError.getValue()
+
+    const inboundInvites = await this.sharedVaultInviteRepository.findBySenderUuidAndSharedVaultUuid({
+      senderUuid: userUuid,
+      sharedVaultUuid,
+    })
+    for (const invite of inboundInvites) {
+      const result = await this.declineInviteToSharedVault.execute({
+        inviteUuid: invite.id.toString(),
+        originatorUuid: userUuid.value,
+      })
+      if (result.isFailed()) {
+        return Result.fail(result.getError())
+      }
+    }
+
+    return Result.ok()
+  }
+}

+ 4 - 0
packages/syncing-server/src/Domain/UseCase/DeleteSharedVaultInvitesSentByUser/DeleteSharedVaultInvitesSentByUserDTO.ts

@@ -0,0 +1,4 @@
+export interface DeleteSharedVaultInvitesSentByUserDTO {
+  userUuid: string
+  sharedVaultUuid: string
+}

+ 83 - 0
packages/syncing-server/src/Domain/UseCase/GetSharedVaultInvitesSentByUser/GetSharedVaultInvitesSentByUser.spec.ts

@@ -0,0 +1,83 @@
+import { Uuid, Timestamps } from '@standardnotes/domain-core'
+
+import { SharedVaultInvite } from '../../SharedVault/User/Invite/SharedVaultInvite'
+import { SharedVaultInviteRepositoryInterface } from '../../SharedVault/User/Invite/SharedVaultInviteRepositoryInterface'
+import { SharedVaultUserPermission } from '../../SharedVault/User/SharedVaultUserPermission'
+import { GetSharedVaultInvitesSentByUser } from './GetSharedVaultInvitesSentByUser'
+
+describe('GetSharedVaultInvitesSentByUser', () => {
+  let sharedVaultInviteRepository: SharedVaultInviteRepositoryInterface
+  let invite: SharedVaultInvite
+
+  const createUseCase = () => new GetSharedVaultInvitesSentByUser(sharedVaultInviteRepository)
+
+  beforeEach(() => {
+    invite = SharedVaultInvite.create({
+      sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+      userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+      senderUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+      encryptedMessage: 'encrypted-message',
+      permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Read).getValue(),
+      timestamps: Timestamps.create(123, 123).getValue(),
+    }).getValue()
+
+    sharedVaultInviteRepository = {} as jest.Mocked<SharedVaultInviteRepositoryInterface>
+    sharedVaultInviteRepository.findBySenderUuid = jest.fn().mockResolvedValue([invite])
+  })
+
+  it('should return invites sent by user', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      senderUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.getValue()).toEqual([invite])
+  })
+
+  it('should return empty array if no invites found', async () => {
+    const useCase = createUseCase()
+
+    sharedVaultInviteRepository.findBySenderUuid = jest.fn().mockResolvedValue([])
+
+    const result = await useCase.execute({
+      senderUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.getValue()).toEqual([])
+  })
+
+  it('should fail if sender uuid is not valid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      senderUuid: 'invalid-uuid',
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+  })
+
+  it('should return invites sent by user for specific shared vault', async () => {
+    const useCase = createUseCase()
+
+    sharedVaultInviteRepository.findBySenderUuidAndSharedVaultUuid = jest.fn().mockResolvedValue([invite])
+
+    const result = await useCase.execute({
+      senderUuid: '00000000-0000-0000-0000-000000000000',
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.getValue()).toEqual([invite])
+  })
+
+  it('should fail if shared vault uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      senderUuid: '00000000-0000-0000-0000-000000000000',
+      sharedVaultUuid: 'invalid-uuid',
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+  })
+})

+ 36 - 0
packages/syncing-server/src/Domain/UseCase/GetSharedVaultInvitesSentByUser/GetSharedVaultInvitesSentByUser.ts

@@ -0,0 +1,36 @@
+import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
+import { SharedVaultInvite } from '../../SharedVault/User/Invite/SharedVaultInvite'
+import { GetSharedVaultInvitesSentByUserDTO } from './GetSharedVaultInvitesSentByUserDTO'
+import { SharedVaultInviteRepositoryInterface } from '../../SharedVault/User/Invite/SharedVaultInviteRepositoryInterface'
+
+export class GetSharedVaultInvitesSentByUser implements UseCaseInterface<SharedVaultInvite[]> {
+  constructor(private sharedVaultInviteRepository: SharedVaultInviteRepositoryInterface) {}
+
+  async execute(dto: GetSharedVaultInvitesSentByUserDTO): Promise<Result<SharedVaultInvite[]>> {
+    const senderUuidOrError = Uuid.create(dto.senderUuid)
+    if (senderUuidOrError.isFailed()) {
+      return Result.fail(senderUuidOrError.getError())
+    }
+    const senderUuid = senderUuidOrError.getValue()
+
+    let sharedVaultUuid: Uuid | undefined
+    if (dto.sharedVaultUuid) {
+      const sharedVaultUuidOrError = Uuid.create(dto.sharedVaultUuid)
+      if (sharedVaultUuidOrError.isFailed()) {
+        return Result.fail(sharedVaultUuidOrError.getError())
+      }
+      sharedVaultUuid = sharedVaultUuidOrError.getValue()
+    }
+
+    if (sharedVaultUuid) {
+      return Result.ok(
+        await this.sharedVaultInviteRepository.findBySenderUuidAndSharedVaultUuid({
+          senderUuid,
+          sharedVaultUuid,
+        }),
+      )
+    }
+
+    return Result.ok(await this.sharedVaultInviteRepository.findBySenderUuid(senderUuid))
+  }
+}

+ 4 - 0
packages/syncing-server/src/Domain/UseCase/GetSharedVaultInvitesSentByUser/GetSharedVaultInvitesSentByUserDTO.ts

@@ -0,0 +1,4 @@
+export interface GetSharedVaultInvitesSentByUserDTO {
+  senderUuid: string
+  sharedVaultUuid?: string
+}

+ 59 - 0
packages/syncing-server/src/Domain/UseCase/GetSharedVaultInvitesSentToUser/GetSharedVaultInvitesSentToUser.spec.ts

@@ -0,0 +1,59 @@
+import { Uuid, Timestamps } from '@standardnotes/domain-core'
+
+import { SharedVaultInvite } from '../../SharedVault/User/Invite/SharedVaultInvite'
+import { SharedVaultInviteRepositoryInterface } from '../../SharedVault/User/Invite/SharedVaultInviteRepositoryInterface'
+import { SharedVaultUserPermission } from '../../SharedVault/User/SharedVaultUserPermission'
+import { GetSharedVaultInvitesSentToUser } from './GetSharedVaultInvitesSentToUser'
+
+describe('GetSharedVaultInvitesSentToUser', () => {
+  let sharedVaultInviteRepository: SharedVaultInviteRepositoryInterface
+  let invite: SharedVaultInvite
+
+  const createUseCase = () => new GetSharedVaultInvitesSentToUser(sharedVaultInviteRepository)
+
+  beforeEach(() => {
+    invite = SharedVaultInvite.create({
+      sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+      userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+      senderUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+      encryptedMessage: 'encrypted-message',
+      permission: SharedVaultUserPermission.create(SharedVaultUserPermission.PERMISSIONS.Read).getValue(),
+      timestamps: Timestamps.create(123, 123).getValue(),
+    }).getValue()
+
+    sharedVaultInviteRepository = {} as jest.Mocked<SharedVaultInviteRepositoryInterface>
+    sharedVaultInviteRepository.findByUserUuid = jest.fn().mockResolvedValue([invite])
+  })
+
+  it('should return invites sent to user', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.getValue()).toEqual([invite])
+  })
+
+  it('should return empty array if no invites found', async () => {
+    const useCase = createUseCase()
+
+    sharedVaultInviteRepository.findByUserUuid = jest.fn().mockReturnValue([])
+
+    const result = await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.getValue()).toEqual([])
+  })
+
+  it('should fail if sender uuid is not valid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: 'invalid-uuid',
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+  })
+})

+ 18 - 0
packages/syncing-server/src/Domain/UseCase/GetSharedVaultInvitesSentToUser/GetSharedVaultInvitesSentToUser.ts

@@ -0,0 +1,18 @@
+import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
+import { SharedVaultInvite } from '../../SharedVault/User/Invite/SharedVaultInvite'
+import { GetSharedVaultInvitesSentToUserDTO } from './GetSharedVaultInvitesSentToUserDTO'
+import { SharedVaultInviteRepositoryInterface } from '../../SharedVault/User/Invite/SharedVaultInviteRepositoryInterface'
+
+export class GetSharedVaultInvitesSentToUser implements UseCaseInterface<SharedVaultInvite[]> {
+  constructor(private sharedVaultInviteRepository: SharedVaultInviteRepositoryInterface) {}
+
+  async execute(dto: GetSharedVaultInvitesSentToUserDTO): Promise<Result<SharedVaultInvite[]>> {
+    const userUuidOrError = Uuid.create(dto.userUuid)
+    if (userUuidOrError.isFailed()) {
+      return Result.fail(userUuidOrError.getError())
+    }
+    const userUuid = userUuidOrError.getValue()
+
+    return Result.ok(await this.sharedVaultInviteRepository.findByUserUuid(userUuid))
+  }
+}

+ 3 - 0
packages/syncing-server/src/Domain/UseCase/GetSharedVaultInvitesSentToUser/GetSharedVaultInvitesSentToUserDTO.ts

@@ -0,0 +1,3 @@
+export interface GetSharedVaultInvitesSentToUserDTO {
+  userUuid: string
+}

+ 4 - 3
packages/syncing-server/src/Domain/UseCase/UpdateSharedVaultInvite/UpdateSharedVaultInvite.ts

@@ -4,14 +4,15 @@ import { SharedVaultInviteRepositoryInterface } from '../../SharedVault/User/Inv
 import { UpdateSharedVaultInviteDTO } from './UpdateSharedVaultInviteDTO'
 import { SharedVaultUserPermission } from '../../SharedVault/User/SharedVaultUserPermission'
 import { TimerInterface } from '@standardnotes/time'
+import { SharedVaultInvite } from '../../SharedVault/User/Invite/SharedVaultInvite'
 
-export class UpdateSharedVaultInvite implements UseCaseInterface<void> {
+export class UpdateSharedVaultInvite implements UseCaseInterface<SharedVaultInvite> {
   constructor(
     private sharedVaultInviteRepository: SharedVaultInviteRepositoryInterface,
     private timer: TimerInterface,
   ) {}
 
-  async execute(dto: UpdateSharedVaultInviteDTO): Promise<Result<void>> {
+  async execute(dto: UpdateSharedVaultInviteDTO): Promise<Result<SharedVaultInvite>> {
     const inviteUuidOrError = Uuid.create(dto.inviteUuid)
     if (inviteUuidOrError.isFailed()) {
       return Result.fail(inviteUuidOrError.getError())
@@ -57,6 +58,6 @@ export class UpdateSharedVaultInvite implements UseCaseInterface<void> {
 
     await this.sharedVaultInviteRepository.save(invite)
 
-    return Result.ok()
+    return Result.ok(invite)
   }
 }

+ 278 - 0
packages/syncing-server/src/Infra/InversifyExpressUtils/HomeServer/HomeServerSharedVaultInvitesController.ts

@@ -0,0 +1,278 @@
+import { Request, Response } from 'express'
+import { BaseHttpController, results } from 'inversify-express-utils'
+import { HttpStatusCode } from '@standardnotes/responses'
+import { ControllerContainerInterface, MapperInterface } from '@standardnotes/domain-core'
+
+import { InviteUserToSharedVault } from '../../../Domain/UseCase/InviteUserToSharedVault/InviteUserToSharedVault'
+import { SharedVaultInvite } from '../../../Domain/SharedVault/User/Invite/SharedVaultInvite'
+import { SharedVaultInviteHttpRepresentation } from '../../../Mapping/Http/SharedVaultInviteHttpRepresentation'
+import { UpdateSharedVaultInvite } from '../../../Domain/UseCase/UpdateSharedVaultInvite/UpdateSharedVaultInvite'
+import { AcceptInviteToSharedVault } from '../../../Domain/UseCase/AcceptInviteToSharedVault/AcceptInviteToSharedVault'
+import { DeclineInviteToSharedVault } from '../../../Domain/UseCase/DeclineInviteToSharedVault/DeclineInviteToSharedVault'
+import { DeleteSharedVaultInvitesToUser } from '../../../Domain/UseCase/DeleteSharedVaultInvitesToUser/DeleteSharedVaultInvitesToUser'
+import { GetSharedVaultInvitesSentByUser } from '../../../Domain/UseCase/GetSharedVaultInvitesSentByUser/GetSharedVaultInvitesSentByUser'
+import { DeleteSharedVaultInvitesSentByUser } from '../../../Domain/UseCase/DeleteSharedVaultInvitesSentByUser/DeleteSharedVaultInvitesSentByUser'
+import { GetSharedVaultInvitesSentToUser } from '../../../Domain/UseCase/GetSharedVaultInvitesSentToUser/GetSharedVaultInvitesSentToUser'
+
+export class HomeServerSharedVaultInvitesController extends BaseHttpController {
+  constructor(
+    protected inviteUserToSharedVaultUseCase: InviteUserToSharedVault,
+    protected updateSharedVaultInviteUseCase: UpdateSharedVaultInvite,
+    protected acceptSharedVaultInviteUseCase: AcceptInviteToSharedVault,
+    protected declineSharedVaultInviteUseCase: DeclineInviteToSharedVault,
+    protected deleteSharedVaultInvitesToUserUseCase: DeleteSharedVaultInvitesToUser,
+    protected deleteSharedVaultInvitesSentByUserUseCase: DeleteSharedVaultInvitesSentByUser,
+    protected getSharedVaultInvitesSentByUserUseCase: GetSharedVaultInvitesSentByUser,
+    protected getSharedVaultInvitesSentToUserUseCase: GetSharedVaultInvitesSentToUser,
+    protected sharedVaultInviteHttpMapper: MapperInterface<SharedVaultInvite, SharedVaultInviteHttpRepresentation>,
+    private controllerContainer?: ControllerContainerInterface,
+  ) {
+    super()
+
+    if (this.controllerContainer !== undefined) {
+      this.controllerContainer.register('sync.shared-vault-invites.create', this.createSharedVaultInvite.bind(this))
+      this.controllerContainer.register('sync.shared-vault-invites.update', this.updateSharedVaultInvite.bind(this))
+      this.controllerContainer.register('sync.shared-vault-invites.accept', this.acceptSharedVaultInvite.bind(this))
+      this.controllerContainer.register('sync.shared-vault-invites.decline', this.declineSharedVaultInvite.bind(this))
+      this.controllerContainer.register(
+        'sync.shared-vault-invites.delete-inbound',
+        this.deleteInboundUserInvites.bind(this),
+      )
+      this.controllerContainer.register(
+        'sync.shared-vault-invites.get-outbound',
+        this.getOutboundUserInvites.bind(this),
+      )
+      this.controllerContainer.register('sync.shared-vault-invites.get-user-invites', this.getUserInvites.bind(this))
+      this.controllerContainer.register(
+        'sync.shared-vault-invites.delete-invite',
+        this.deleteSharedVaultInvite.bind(this),
+      )
+      this.controllerContainer.register(
+        'sync.shared-vault-invites.delete-all',
+        this.deleteAllSharedVaultInvites.bind(this),
+      )
+    }
+  }
+
+  async createSharedVaultInvite(request: Request, response: Response): Promise<results.JsonResult> {
+    const result = await this.inviteUserToSharedVaultUseCase.execute({
+      sharedVaultUuid: request.params.sharedVaultUuid,
+      senderUuid: response.locals.user.uuid,
+      recipientUuid: request.body.recipient_uid,
+      encryptedMessage: request.body.encrypted_message,
+      permission: request.body.permission,
+    })
+
+    if (result.isFailed()) {
+      return this.json(
+        {
+          error: {
+            message: result.getError(),
+          },
+        },
+        HttpStatusCode.BadRequest,
+      )
+    }
+
+    return this.json({
+      invite: this.sharedVaultInviteHttpMapper.toProjection(result.getValue()),
+    })
+  }
+
+  async updateSharedVaultInvite(request: Request, response: Response): Promise<results.JsonResult> {
+    const result = await this.updateSharedVaultInviteUseCase.execute({
+      encryptedMessage: request.body.encrypted_message,
+      inviteUuid: request.params.inviteUuid,
+      senderUuid: response.locals.user.uuid,
+      permission: request.body.permission,
+    })
+
+    if (result.isFailed()) {
+      return this.json(
+        {
+          error: {
+            message: result.getError(),
+          },
+        },
+        HttpStatusCode.BadRequest,
+      )
+    }
+
+    return this.json({
+      invite: this.sharedVaultInviteHttpMapper.toProjection(result.getValue()),
+    })
+  }
+
+  async acceptSharedVaultInvite(request: Request, response: Response): Promise<results.JsonResult> {
+    const result = await this.acceptSharedVaultInviteUseCase.execute({
+      inviteUuid: request.params.inviteUuid,
+      originatorUuid: response.locals.user.uuid,
+    })
+
+    if (result.isFailed()) {
+      return this.json(
+        {
+          error: {
+            message: result.getError(),
+          },
+        },
+        HttpStatusCode.BadRequest,
+      )
+    }
+
+    return this.json({
+      success: true,
+    })
+  }
+
+  async declineSharedVaultInvite(request: Request, response: Response): Promise<results.JsonResult> {
+    const result = await this.declineSharedVaultInviteUseCase.execute({
+      inviteUuid: request.params.inviteUuid,
+      originatorUuid: response.locals.user.uuid,
+    })
+
+    if (result.isFailed()) {
+      return this.json(
+        {
+          error: {
+            message: result.getError(),
+          },
+        },
+        HttpStatusCode.BadRequest,
+      )
+    }
+
+    return this.json({
+      success: true,
+    })
+  }
+
+  async deleteInboundUserInvites(_request: Request, response: Response): Promise<results.JsonResult> {
+    const result = await this.deleteSharedVaultInvitesToUserUseCase.execute({
+      userUuid: response.locals.user.uuid,
+    })
+
+    if (result.isFailed()) {
+      return this.json(
+        {
+          error: {
+            message: result.getError(),
+          },
+        },
+        HttpStatusCode.BadRequest,
+      )
+    }
+
+    return this.json({
+      success: true,
+    })
+  }
+
+  async getOutboundUserInvites(_request: Request, response: Response): Promise<results.JsonResult> {
+    const result = await this.getSharedVaultInvitesSentByUserUseCase.execute({
+      senderUuid: response.locals.user.uuid,
+    })
+
+    if (result.isFailed()) {
+      return this.json(
+        {
+          error: {
+            message: result.getError(),
+          },
+        },
+        HttpStatusCode.BadRequest,
+      )
+    }
+
+    return this.json({
+      invites: result.getValue().map((invite) => this.sharedVaultInviteHttpMapper.toProjection(invite)),
+    })
+  }
+
+  async getSharedVaultInvites(request: Request, response: Response): Promise<results.JsonResult> {
+    const result = await this.getSharedVaultInvitesSentByUserUseCase.execute({
+      senderUuid: response.locals.user.uuid,
+      sharedVaultUuid: request.params.sharedVaultUuid,
+    })
+
+    if (result.isFailed()) {
+      return this.json(
+        {
+          error: {
+            message: result.getError(),
+          },
+        },
+        HttpStatusCode.BadRequest,
+      )
+    }
+
+    return this.json({
+      invites: result.getValue().map((invite) => this.sharedVaultInviteHttpMapper.toProjection(invite)),
+    })
+  }
+
+  async getUserInvites(_request: Request, response: Response): Promise<results.JsonResult> {
+    const result = await this.getSharedVaultInvitesSentToUserUseCase.execute({
+      userUuid: response.locals.user.uuid,
+    })
+
+    if (result.isFailed()) {
+      return this.json(
+        {
+          error: {
+            message: result.getError(),
+          },
+        },
+        HttpStatusCode.BadRequest,
+      )
+    }
+
+    return this.json({
+      invites: result.getValue().map((invite) => this.sharedVaultInviteHttpMapper.toProjection(invite)),
+    })
+  }
+
+  async deleteSharedVaultInvite(request: Request, response: Response): Promise<results.JsonResult> {
+    const result = await this.declineSharedVaultInviteUseCase.execute({
+      inviteUuid: request.params.inviteUuid,
+      originatorUuid: response.locals.user.uuid,
+    })
+
+    if (result.isFailed()) {
+      return this.json(
+        {
+          error: {
+            message: result.getError(),
+          },
+        },
+        HttpStatusCode.BadRequest,
+      )
+    }
+
+    return this.json({
+      success: true,
+    })
+  }
+
+  async deleteAllSharedVaultInvites(request: Request, response: Response): Promise<results.JsonResult> {
+    const result = await this.deleteSharedVaultInvitesSentByUserUseCase.execute({
+      userUuid: response.locals.user.uuid,
+      sharedVaultUuid: request.params.sharedVaultUuid,
+    })
+
+    if (result.isFailed()) {
+      return this.json(
+        {
+          error: {
+            message: result.getError(),
+          },
+        },
+        HttpStatusCode.BadRequest,
+      )
+    }
+
+    return this.json({
+      success: true,
+    })
+  }
+}

+ 99 - 0
packages/syncing-server/src/Infra/InversifyExpressUtils/InversifyExpressSharedVaultInvitesController.ts

@@ -0,0 +1,99 @@
+import { controller, httpDelete, httpGet, httpPatch, httpPost, results } from 'inversify-express-utils'
+import { MapperInterface } from '@standardnotes/domain-core'
+import { Request, Response } from 'express'
+
+import TYPES from '../../Bootstrap/Types'
+import { SharedVaultInvite } from '../../Domain/SharedVault/User/Invite/SharedVaultInvite'
+import { AcceptInviteToSharedVault } from '../../Domain/UseCase/AcceptInviteToSharedVault/AcceptInviteToSharedVault'
+import { DeclineInviteToSharedVault } from '../../Domain/UseCase/DeclineInviteToSharedVault/DeclineInviteToSharedVault'
+import { DeleteSharedVaultInvitesSentByUser } from '../../Domain/UseCase/DeleteSharedVaultInvitesSentByUser/DeleteSharedVaultInvitesSentByUser'
+import { DeleteSharedVaultInvitesToUser } from '../../Domain/UseCase/DeleteSharedVaultInvitesToUser/DeleteSharedVaultInvitesToUser'
+import { GetSharedVaultInvitesSentByUser } from '../../Domain/UseCase/GetSharedVaultInvitesSentByUser/GetSharedVaultInvitesSentByUser'
+import { InviteUserToSharedVault } from '../../Domain/UseCase/InviteUserToSharedVault/InviteUserToSharedVault'
+import { UpdateSharedVaultInvite } from '../../Domain/UseCase/UpdateSharedVaultInvite/UpdateSharedVaultInvite'
+import { SharedVaultInviteHttpRepresentation } from '../../Mapping/Http/SharedVaultInviteHttpRepresentation'
+import { HomeServerSharedVaultInvitesController } from './HomeServer/HomeServerSharedVaultInvitesController'
+import { GetSharedVaultInvitesSentToUser } from '../../Domain/UseCase/GetSharedVaultInvitesSentToUser/GetSharedVaultInvitesSentToUser'
+import { inject } from 'inversify'
+
+@controller('/shared-vaults', TYPES.Sync_AuthMiddleware)
+export class InversifyExpressSharedVaultInvitesController extends HomeServerSharedVaultInvitesController {
+  constructor(
+    @inject(TYPES.Sync_InviteUserToSharedVault) override inviteUserToSharedVaultUseCase: InviteUserToSharedVault,
+    @inject(TYPES.Sync_UpdateSharedVaultInvite) override updateSharedVaultInviteUseCase: UpdateSharedVaultInvite,
+    @inject(TYPES.Sync_AcceptInviteToSharedVault) override acceptSharedVaultInviteUseCase: AcceptInviteToSharedVault,
+    @inject(TYPES.Sync_DeclineInviteToSharedVault) override declineSharedVaultInviteUseCase: DeclineInviteToSharedVault,
+    @inject(TYPES.Sync_DeleteSharedVaultInvitesToUser)
+    override deleteSharedVaultInvitesToUserUseCase: DeleteSharedVaultInvitesToUser,
+    @inject(TYPES.Sync_DeleteSharedVaultInvitesSentByUser)
+    override deleteSharedVaultInvitesSentByUserUseCase: DeleteSharedVaultInvitesSentByUser,
+    @inject(TYPES.Sync_GetSharedVaultInvitesSentByUser)
+    override getSharedVaultInvitesSentByUserUseCase: GetSharedVaultInvitesSentByUser,
+    @inject(TYPES.Sync_GetSharedVaultInvitesSentToUser)
+    override getSharedVaultInvitesSentToUserUseCase: GetSharedVaultInvitesSentToUser,
+    @inject(TYPES.Sync_SharedVaultInviteHttpMapper)
+    override sharedVaultInviteHttpMapper: MapperInterface<SharedVaultInvite, SharedVaultInviteHttpRepresentation>,
+  ) {
+    super(
+      inviteUserToSharedVaultUseCase,
+      updateSharedVaultInviteUseCase,
+      acceptSharedVaultInviteUseCase,
+      declineSharedVaultInviteUseCase,
+      deleteSharedVaultInvitesToUserUseCase,
+      deleteSharedVaultInvitesSentByUserUseCase,
+      getSharedVaultInvitesSentByUserUseCase,
+      getSharedVaultInvitesSentToUserUseCase,
+      sharedVaultInviteHttpMapper,
+    )
+  }
+
+  @httpPost('/:sharedVaultUuid/invites')
+  override async createSharedVaultInvite(request: Request, response: Response): Promise<results.JsonResult> {
+    return super.createSharedVaultInvite(request, response)
+  }
+
+  @httpPatch('/:sharedVaultUuid/invites/:inviteUuid')
+  override async updateSharedVaultInvite(request: Request, response: Response): Promise<results.JsonResult> {
+    return super.updateSharedVaultInvite(request, response)
+  }
+
+  @httpPost('/:sharedVaultUuid/invites/:inviteUuid/accept')
+  override async acceptSharedVaultInvite(request: Request, response: Response): Promise<results.JsonResult> {
+    return super.acceptSharedVaultInvite(request, response)
+  }
+
+  @httpPost('/:sharedVaultUuid/invites/:inviteUuid/decline')
+  override async declineSharedVaultInvite(request: Request, response: Response): Promise<results.JsonResult> {
+    return super.declineSharedVaultInvite(request, response)
+  }
+
+  @httpDelete('/invites/inbound')
+  override async deleteInboundUserInvites(request: Request, response: Response): Promise<results.JsonResult> {
+    return super.deleteInboundUserInvites(request, response)
+  }
+
+  @httpGet('/invites/outbound')
+  override async getOutboundUserInvites(request: Request, response: Response): Promise<results.JsonResult> {
+    return super.getOutboundUserInvites(request, response)
+  }
+
+  @httpGet('/invites')
+  override async getUserInvites(request: Request, response: Response): Promise<results.JsonResult> {
+    return super.getUserInvites(request, response)
+  }
+
+  @httpGet('/:sharedVaultUuid/invites')
+  override async getSharedVaultInvites(request: Request, response: Response): Promise<results.JsonResult> {
+    return super.getSharedVaultInvites(request, response)
+  }
+
+  @httpDelete('/:sharedVaultUuid/invites/:inviteUuid')
+  override async deleteSharedVaultInvite(request: Request, response: Response): Promise<results.JsonResult> {
+    return super.deleteSharedVaultInvite(request, response)
+  }
+
+  @httpDelete('/:sharedVaultUuid/invites')
+  override async deleteAllSharedVaultInvites(request: Request, response: Response): Promise<results.JsonResult> {
+    return super.deleteAllSharedVaultInvites(request, response)
+  }
+}

+ 28 - 0
packages/syncing-server/src/Infra/TypeORM/TypeORMSharedVaultInviteRepository.ts

@@ -11,6 +11,34 @@ export class TypeORMSharedVaultInviteRepository implements SharedVaultInviteRepo
     private mapper: MapperInterface<SharedVaultInvite, TypeORMSharedVaultInvite>,
   ) {}
 
+  async findBySenderUuidAndSharedVaultUuid(dto: {
+    senderUuid: Uuid
+    sharedVaultUuid: Uuid
+  }): Promise<SharedVaultInvite[]> {
+    const persistence = await this.ormRepository
+      .createQueryBuilder('shared_vault_invite')
+      .where('shared_vault_invite.sender_uuid = :uuid', {
+        senderUuid: dto.senderUuid.value,
+      })
+      .andWhere('shared_vault_invite.shared_vault_uuid = :sharedVaultUuid', {
+        sharedVaultUuid: dto.sharedVaultUuid.value,
+      })
+      .getMany()
+
+    return persistence.map((p) => this.mapper.toDomain(p))
+  }
+
+  async findBySenderUuid(senderUuid: Uuid): Promise<SharedVaultInvite[]> {
+    const persistence = await this.ormRepository
+      .createQueryBuilder('shared_vault_invite')
+      .where('shared_vault_invite.sender_uuid = :senderUuid', {
+        senderUuid: senderUuid.value,
+      })
+      .getMany()
+
+    return persistence.map((p) => this.mapper.toDomain(p))
+  }
+
   async findByUserUuid(userUuid: Uuid): Promise<SharedVaultInvite[]> {
     const persistence = await this.ormRepository
       .createQueryBuilder('shared_vault_invite')

+ 25 - 0
packages/syncing-server/src/Mapping/Http/SharedVaultInviteHttpMapper.ts

@@ -0,0 +1,25 @@
+import { MapperInterface } from '@standardnotes/domain-core'
+
+import { SharedVaultInvite } from '../../Domain/SharedVault/User/Invite/SharedVaultInvite'
+import { SharedVaultInviteHttpRepresentation } from './SharedVaultInviteHttpRepresentation'
+
+export class SharedVaultInviteHttpMapper
+  implements MapperInterface<SharedVaultInvite, SharedVaultInviteHttpRepresentation>
+{
+  toDomain(_projection: SharedVaultInviteHttpRepresentation): SharedVaultInvite {
+    throw new Error('Mapping from http representation to domain is not implemented.')
+  }
+
+  toProjection(domain: SharedVaultInvite): SharedVaultInviteHttpRepresentation {
+    return {
+      uuid: domain.id.toString(),
+      shared_vault_uuid: domain.props.sharedVaultUuid.value,
+      user_uuid: domain.props.userUuid.value,
+      sender_uuid: domain.props.senderUuid.value,
+      encrypted_message: domain.props.encryptedMessage,
+      permissions: domain.props.permission.value,
+      created_at_timestamp: domain.props.timestamps.createdAt,
+      updated_at_timestamp: domain.props.timestamps.updatedAt,
+    }
+  }
+}

+ 10 - 0
packages/syncing-server/src/Mapping/Http/SharedVaultInviteHttpRepresentation.ts

@@ -0,0 +1,10 @@
+export interface SharedVaultInviteHttpRepresentation {
+  uuid: string
+  shared_vault_uuid: string
+  user_uuid: string
+  sender_uuid: string
+  encrypted_message: string
+  permissions: string
+  created_at_timestamp: number
+  updated_at_timestamp: number
+}