diff --git a/packages/syncing-server/src/Bootstrap/Types.ts b/packages/syncing-server/src/Bootstrap/Types.ts index 5dadd91fa..c1ce73cdf 100644 --- a/packages/syncing-server/src/Bootstrap/Types.ts +++ b/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'), diff --git a/packages/syncing-server/src/Domain/SharedVault/User/Invite/SharedVaultInviteRepositoryInterface.ts b/packages/syncing-server/src/Domain/SharedVault/User/Invite/SharedVaultInviteRepositoryInterface.ts index b7520dea1..6de4ed30b 100644 --- a/packages/syncing-server/src/Domain/SharedVault/User/Invite/SharedVaultInviteRepositoryInterface.ts +++ b/packages/syncing-server/src/Domain/SharedVault/User/Invite/SharedVaultInviteRepositoryInterface.ts @@ -8,5 +8,7 @@ export interface SharedVaultInviteRepositoryInterface { remove(sharedVaultInvite: SharedVaultInvite): Promise removeBySharedVaultUuid(sharedVaultUuid: Uuid): Promise findByUserUuid(userUuid: Uuid): Promise + findBySenderUuid(senderUuid: Uuid): Promise findByUserUuidAndSharedVaultUuid(dto: { userUuid: Uuid; sharedVaultUuid: Uuid }): Promise + findBySenderUuidAndSharedVaultUuid(dto: { senderUuid: Uuid; sharedVaultUuid: Uuid }): Promise } diff --git a/packages/syncing-server/src/Domain/UseCase/DeleteSharedVaultInvitesSentByUser/DeleteSharedVaultInvitesSentByUser.spec.ts b/packages/syncing-server/src/Domain/UseCase/DeleteSharedVaultInvitesSentByUser/DeleteSharedVaultInvitesSentByUser.spec.ts new file mode 100644 index 000000000..2bb0ecd60 --- /dev/null +++ b/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 + sharedVaultInviteRepository.findBySenderUuidAndSharedVaultUuid = jest.fn().mockReturnValue([sharedVaultInvite]) + + declineInviteToSharedVault = {} as jest.Mocked + 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() + }) +}) diff --git a/packages/syncing-server/src/Domain/UseCase/DeleteSharedVaultInvitesSentByUser/DeleteSharedVaultInvitesSentByUser.ts b/packages/syncing-server/src/Domain/UseCase/DeleteSharedVaultInvitesSentByUser/DeleteSharedVaultInvitesSentByUser.ts new file mode 100644 index 000000000..a6ea22943 --- /dev/null +++ b/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 { + constructor( + private sharedVaultInviteRepository: SharedVaultInviteRepositoryInterface, + private declineInviteToSharedVault: DeclineInviteToSharedVault, + ) {} + + async execute(dto: DeleteSharedVaultInvitesSentByUserDTO): Promise> { + 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() + } +} diff --git a/packages/syncing-server/src/Domain/UseCase/DeleteSharedVaultInvitesSentByUser/DeleteSharedVaultInvitesSentByUserDTO.ts b/packages/syncing-server/src/Domain/UseCase/DeleteSharedVaultInvitesSentByUser/DeleteSharedVaultInvitesSentByUserDTO.ts new file mode 100644 index 000000000..f8c1b61b4 --- /dev/null +++ b/packages/syncing-server/src/Domain/UseCase/DeleteSharedVaultInvitesSentByUser/DeleteSharedVaultInvitesSentByUserDTO.ts @@ -0,0 +1,4 @@ +export interface DeleteSharedVaultInvitesSentByUserDTO { + userUuid: string + sharedVaultUuid: string +} diff --git a/packages/syncing-server/src/Domain/UseCase/GetSharedVaultInvitesSentByUser/GetSharedVaultInvitesSentByUser.spec.ts b/packages/syncing-server/src/Domain/UseCase/GetSharedVaultInvitesSentByUser/GetSharedVaultInvitesSentByUser.spec.ts new file mode 100644 index 000000000..b59637ff7 --- /dev/null +++ b/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 + 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() + }) +}) diff --git a/packages/syncing-server/src/Domain/UseCase/GetSharedVaultInvitesSentByUser/GetSharedVaultInvitesSentByUser.ts b/packages/syncing-server/src/Domain/UseCase/GetSharedVaultInvitesSentByUser/GetSharedVaultInvitesSentByUser.ts new file mode 100644 index 000000000..04ff7bc69 --- /dev/null +++ b/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 { + constructor(private sharedVaultInviteRepository: SharedVaultInviteRepositoryInterface) {} + + async execute(dto: GetSharedVaultInvitesSentByUserDTO): Promise> { + 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)) + } +} diff --git a/packages/syncing-server/src/Domain/UseCase/GetSharedVaultInvitesSentByUser/GetSharedVaultInvitesSentByUserDTO.ts b/packages/syncing-server/src/Domain/UseCase/GetSharedVaultInvitesSentByUser/GetSharedVaultInvitesSentByUserDTO.ts new file mode 100644 index 000000000..d82ea2a15 --- /dev/null +++ b/packages/syncing-server/src/Domain/UseCase/GetSharedVaultInvitesSentByUser/GetSharedVaultInvitesSentByUserDTO.ts @@ -0,0 +1,4 @@ +export interface GetSharedVaultInvitesSentByUserDTO { + senderUuid: string + sharedVaultUuid?: string +} diff --git a/packages/syncing-server/src/Domain/UseCase/GetSharedVaultInvitesSentToUser/GetSharedVaultInvitesSentToUser.spec.ts b/packages/syncing-server/src/Domain/UseCase/GetSharedVaultInvitesSentToUser/GetSharedVaultInvitesSentToUser.spec.ts new file mode 100644 index 000000000..1d5f86e86 --- /dev/null +++ b/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 + 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() + }) +}) diff --git a/packages/syncing-server/src/Domain/UseCase/GetSharedVaultInvitesSentToUser/GetSharedVaultInvitesSentToUser.ts b/packages/syncing-server/src/Domain/UseCase/GetSharedVaultInvitesSentToUser/GetSharedVaultInvitesSentToUser.ts new file mode 100644 index 000000000..7ac5e91a3 --- /dev/null +++ b/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 { + constructor(private sharedVaultInviteRepository: SharedVaultInviteRepositoryInterface) {} + + async execute(dto: GetSharedVaultInvitesSentToUserDTO): Promise> { + 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)) + } +} diff --git a/packages/syncing-server/src/Domain/UseCase/GetSharedVaultInvitesSentToUser/GetSharedVaultInvitesSentToUserDTO.ts b/packages/syncing-server/src/Domain/UseCase/GetSharedVaultInvitesSentToUser/GetSharedVaultInvitesSentToUserDTO.ts new file mode 100644 index 000000000..8a1c45c1c --- /dev/null +++ b/packages/syncing-server/src/Domain/UseCase/GetSharedVaultInvitesSentToUser/GetSharedVaultInvitesSentToUserDTO.ts @@ -0,0 +1,3 @@ +export interface GetSharedVaultInvitesSentToUserDTO { + userUuid: string +} diff --git a/packages/syncing-server/src/Domain/UseCase/UpdateSharedVaultInvite/UpdateSharedVaultInvite.ts b/packages/syncing-server/src/Domain/UseCase/UpdateSharedVaultInvite/UpdateSharedVaultInvite.ts index b2bc484b3..c928a7373 100644 --- a/packages/syncing-server/src/Domain/UseCase/UpdateSharedVaultInvite/UpdateSharedVaultInvite.ts +++ b/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 { +export class UpdateSharedVaultInvite implements UseCaseInterface { constructor( private sharedVaultInviteRepository: SharedVaultInviteRepositoryInterface, private timer: TimerInterface, ) {} - async execute(dto: UpdateSharedVaultInviteDTO): Promise> { + async execute(dto: UpdateSharedVaultInviteDTO): Promise> { const inviteUuidOrError = Uuid.create(dto.inviteUuid) if (inviteUuidOrError.isFailed()) { return Result.fail(inviteUuidOrError.getError()) @@ -57,6 +58,6 @@ export class UpdateSharedVaultInvite implements UseCaseInterface { await this.sharedVaultInviteRepository.save(invite) - return Result.ok() + return Result.ok(invite) } } diff --git a/packages/syncing-server/src/Infra/InversifyExpressUtils/HomeServer/HomeServerSharedVaultInvitesController.ts b/packages/syncing-server/src/Infra/InversifyExpressUtils/HomeServer/HomeServerSharedVaultInvitesController.ts new file mode 100644 index 000000000..649c44383 --- /dev/null +++ b/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, + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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, + }) + } +} diff --git a/packages/syncing-server/src/Infra/InversifyExpressUtils/InversifyExpressSharedVaultInvitesController.ts b/packages/syncing-server/src/Infra/InversifyExpressUtils/InversifyExpressSharedVaultInvitesController.ts new file mode 100644 index 000000000..8a958447b --- /dev/null +++ b/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, + ) { + super( + inviteUserToSharedVaultUseCase, + updateSharedVaultInviteUseCase, + acceptSharedVaultInviteUseCase, + declineSharedVaultInviteUseCase, + deleteSharedVaultInvitesToUserUseCase, + deleteSharedVaultInvitesSentByUserUseCase, + getSharedVaultInvitesSentByUserUseCase, + getSharedVaultInvitesSentToUserUseCase, + sharedVaultInviteHttpMapper, + ) + } + + @httpPost('/:sharedVaultUuid/invites') + override async createSharedVaultInvite(request: Request, response: Response): Promise { + return super.createSharedVaultInvite(request, response) + } + + @httpPatch('/:sharedVaultUuid/invites/:inviteUuid') + override async updateSharedVaultInvite(request: Request, response: Response): Promise { + return super.updateSharedVaultInvite(request, response) + } + + @httpPost('/:sharedVaultUuid/invites/:inviteUuid/accept') + override async acceptSharedVaultInvite(request: Request, response: Response): Promise { + return super.acceptSharedVaultInvite(request, response) + } + + @httpPost('/:sharedVaultUuid/invites/:inviteUuid/decline') + override async declineSharedVaultInvite(request: Request, response: Response): Promise { + return super.declineSharedVaultInvite(request, response) + } + + @httpDelete('/invites/inbound') + override async deleteInboundUserInvites(request: Request, response: Response): Promise { + return super.deleteInboundUserInvites(request, response) + } + + @httpGet('/invites/outbound') + override async getOutboundUserInvites(request: Request, response: Response): Promise { + return super.getOutboundUserInvites(request, response) + } + + @httpGet('/invites') + override async getUserInvites(request: Request, response: Response): Promise { + return super.getUserInvites(request, response) + } + + @httpGet('/:sharedVaultUuid/invites') + override async getSharedVaultInvites(request: Request, response: Response): Promise { + return super.getSharedVaultInvites(request, response) + } + + @httpDelete('/:sharedVaultUuid/invites/:inviteUuid') + override async deleteSharedVaultInvite(request: Request, response: Response): Promise { + return super.deleteSharedVaultInvite(request, response) + } + + @httpDelete('/:sharedVaultUuid/invites') + override async deleteAllSharedVaultInvites(request: Request, response: Response): Promise { + return super.deleteAllSharedVaultInvites(request, response) + } +} diff --git a/packages/syncing-server/src/Infra/TypeORM/TypeORMSharedVaultInviteRepository.ts b/packages/syncing-server/src/Infra/TypeORM/TypeORMSharedVaultInviteRepository.ts index ee8e8797c..d394f8062 100644 --- a/packages/syncing-server/src/Infra/TypeORM/TypeORMSharedVaultInviteRepository.ts +++ b/packages/syncing-server/src/Infra/TypeORM/TypeORMSharedVaultInviteRepository.ts @@ -11,6 +11,34 @@ export class TypeORMSharedVaultInviteRepository implements SharedVaultInviteRepo private mapper: MapperInterface, ) {} + async findBySenderUuidAndSharedVaultUuid(dto: { + senderUuid: Uuid + sharedVaultUuid: Uuid + }): Promise { + 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 { + 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 { const persistence = await this.ormRepository .createQueryBuilder('shared_vault_invite') diff --git a/packages/syncing-server/src/Mapping/Http/SharedVaultInviteHttpMapper.ts b/packages/syncing-server/src/Mapping/Http/SharedVaultInviteHttpMapper.ts new file mode 100644 index 000000000..2a0b22870 --- /dev/null +++ b/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 +{ + 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, + } + } +} diff --git a/packages/syncing-server/src/Mapping/Http/SharedVaultInviteHttpRepresentation.ts b/packages/syncing-server/src/Mapping/Http/SharedVaultInviteHttpRepresentation.ts new file mode 100644 index 000000000..509d9e568 --- /dev/null +++ b/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 +}