diff --git a/packages/workspace/src/Domain/Invite/WorkspaceInviteRepositoryInterface.ts b/packages/workspace/src/Domain/Invite/WorkspaceInviteRepositoryInterface.ts index 40625a642..6f4848d6f 100644 --- a/packages/workspace/src/Domain/Invite/WorkspaceInviteRepositoryInterface.ts +++ b/packages/workspace/src/Domain/Invite/WorkspaceInviteRepositoryInterface.ts @@ -1,5 +1,8 @@ +import { Uuid } from '@standardnotes/common' + import { WorkspaceInvite } from './WorkspaceInvite' export interface WorkspaceInviteRepositoryInterface { + findOneByUuid(uuid: Uuid): Promise save(workspace: WorkspaceInvite): Promise } diff --git a/packages/workspace/src/Domain/UseCase/AcceptInvitation/AcceptInvitation.spec.ts b/packages/workspace/src/Domain/UseCase/AcceptInvitation/AcceptInvitation.spec.ts new file mode 100644 index 000000000..145549ba7 --- /dev/null +++ b/packages/workspace/src/Domain/UseCase/AcceptInvitation/AcceptInvitation.spec.ts @@ -0,0 +1,71 @@ +import 'reflect-metadata' +import { TimerInterface } from '@standardnotes/time' + +import { WorkspaceInvite } from '../../Invite/WorkspaceInvite' +import { WorkspaceInviteRepositoryInterface } from '../../Invite/WorkspaceInviteRepositoryInterface' +import { WorkspaceUserRepositoryInterface } from '../../Workspace/WorkspaceUserRepositoryInterface' + +import { AcceptInvitation } from './AcceptInvitation' + +describe('AcceptInvitation', () => { + let workspaceInviteRepository: WorkspaceInviteRepositoryInterface + let workspaceUserRepository: WorkspaceUserRepositoryInterface + let timer: TimerInterface + let invite: WorkspaceInvite + + const createUseCase = () => new AcceptInvitation(workspaceInviteRepository, workspaceUserRepository, timer) + + beforeEach(() => { + invite = { + uuid: 'i-1-2-3', + workspaceUuid: 'w-1-2-3', + } as jest.Mocked + workspaceInviteRepository = {} as jest.Mocked + workspaceInviteRepository.findOneByUuid = jest.fn().mockReturnValue(invite) + workspaceInviteRepository.save = jest.fn() + + workspaceUserRepository = {} as jest.Mocked + workspaceUserRepository.save = jest.fn() + + timer = {} as jest.Mocked + timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(1) + }) + + it('should accept an invite and assign user to workspace', async () => { + await createUseCase().execute({ + acceptingUserUuid: 'u-1-2-3', + encryptedPrivateKey: 'foo', + publicKey: 'bar', + invitationUuid: 'i-1-2-3', + }) + + expect(workspaceInviteRepository.save).toHaveBeenCalledWith({ + acceptingUserUuid: 'u-1-2-3', + status: 'accepted', + updatedAt: 1, + uuid: 'i-1-2-3', + workspaceUuid: 'w-1-2-3', + }) + expect(workspaceUserRepository.save).toHaveBeenCalledWith({ + encryptedPrivateKey: 'foo', + publicKey: 'bar', + status: 'pending-keyshare', + userUuid: 'u-1-2-3', + workspaceUuid: 'w-1-2-3', + }) + }) + + it('should not accept an invite if it does not exist', async () => { + workspaceInviteRepository.findOneByUuid = jest.fn().mockReturnValue(null) + + await createUseCase().execute({ + acceptingUserUuid: 'u-1-2-3', + encryptedPrivateKey: 'foo', + publicKey: 'bar', + invitationUuid: 'i-1-2-3', + }) + + expect(workspaceInviteRepository.save).not.toHaveBeenCalled() + expect(workspaceUserRepository.save).not.toHaveBeenCalled() + }) +}) diff --git a/packages/workspace/src/Domain/UseCase/AcceptInvitation/AcceptInvitation.ts b/packages/workspace/src/Domain/UseCase/AcceptInvitation/AcceptInvitation.ts new file mode 100644 index 000000000..0d7514e77 --- /dev/null +++ b/packages/workspace/src/Domain/UseCase/AcceptInvitation/AcceptInvitation.ts @@ -0,0 +1,50 @@ +import { TimerInterface } from '@standardnotes/time' +import { inject, injectable } from 'inversify' + +import TYPES from '../../../Bootstrap/Types' +import { WorkspaceInviteRepositoryInterface } from '../../Invite/WorkspaceInviteRepositoryInterface' +import { WorkspaceInviteStatus } from '../../Invite/WorkspaceInviteStatus' +import { WorkspaceUser } from '../../Workspace/WorkspaceUser' +import { WorkspaceUserRepositoryInterface } from '../../Workspace/WorkspaceUserRepositoryInterface' +import { WorkspaceUserStatus } from '../../Workspace/WorkspaceUserStatus' +import { UseCaseInterface } from '../UseCaseInterface' + +import { AcceptInvitationDTO } from './AcceptInvitationDTO' +import { AcceptInvitationResponse } from './AcceptInvitationResponse' + +@injectable() +export class AcceptInvitation implements UseCaseInterface { + constructor( + @inject(TYPES.WorkspaceInviteRepository) private workspaceInviteRepository: WorkspaceInviteRepositoryInterface, + @inject(TYPES.WorkspaceUserRepository) private workspaceUserRepository: WorkspaceUserRepositoryInterface, + @inject(TYPES.Timer) private timer: TimerInterface, + ) {} + + async execute(dto: AcceptInvitationDTO): Promise { + const invite = await this.workspaceInviteRepository.findOneByUuid(dto.invitationUuid) + if (invite === null) { + return { + success: false, + } + } + + invite.acceptingUserUuid = dto.acceptingUserUuid + invite.updatedAt = this.timer.getTimestampInMicroseconds() + invite.status = WorkspaceInviteStatus.Accepted + + await this.workspaceInviteRepository.save(invite) + + const workspaceUser = new WorkspaceUser() + workspaceUser.userUuid = dto.acceptingUserUuid + workspaceUser.workspaceUuid = invite.workspaceUuid + workspaceUser.publicKey = dto.publicKey + workspaceUser.encryptedPrivateKey = dto.encryptedPrivateKey + workspaceUser.status = WorkspaceUserStatus.PendingKeyshare + + await this.workspaceUserRepository.save(workspaceUser) + + return { + success: true, + } + } +} diff --git a/packages/workspace/src/Domain/UseCase/AcceptInvitation/AcceptInvitationDTO.ts b/packages/workspace/src/Domain/UseCase/AcceptInvitation/AcceptInvitationDTO.ts new file mode 100644 index 000000000..b36db28ea --- /dev/null +++ b/packages/workspace/src/Domain/UseCase/AcceptInvitation/AcceptInvitationDTO.ts @@ -0,0 +1,8 @@ +import { Uuid } from '@standardnotes/common' + +export type AcceptInvitationDTO = { + invitationUuid: Uuid + acceptingUserUuid: Uuid + publicKey: string + encryptedPrivateKey: string +} diff --git a/packages/workspace/src/Domain/UseCase/AcceptInvitation/AcceptInvitationResponse.ts b/packages/workspace/src/Domain/UseCase/AcceptInvitation/AcceptInvitationResponse.ts new file mode 100644 index 000000000..adf77d586 --- /dev/null +++ b/packages/workspace/src/Domain/UseCase/AcceptInvitation/AcceptInvitationResponse.ts @@ -0,0 +1,3 @@ +export type AcceptInvitationResponse = { + success: boolean +} diff --git a/packages/workspace/src/Infra/MySQL/MySQLWorkspaceInviteRepository.spec.ts b/packages/workspace/src/Infra/MySQL/MySQLWorkspaceInviteRepository.spec.ts index 60c2c2409..e50ef9b84 100644 --- a/packages/workspace/src/Infra/MySQL/MySQLWorkspaceInviteRepository.spec.ts +++ b/packages/workspace/src/Infra/MySQL/MySQLWorkspaceInviteRepository.spec.ts @@ -27,4 +27,13 @@ describe('MySQLWorkspaceInviteRepository', () => { expect(ormRepository.save).toHaveBeenCalledWith(invite) }) + + it('should find one by uuid', async () => { + queryBuilder.where = jest.fn().mockReturnThis() + queryBuilder.getOne = jest.fn().mockReturnValue(null) + + await createRepository().findOneByUuid('i-1-2-3') + + expect(queryBuilder.where).toHaveBeenCalledWith('uuid = :uuid', { uuid: 'i-1-2-3' }) + }) }) diff --git a/packages/workspace/src/Infra/MySQL/MySQLWorkspaceInviteRepository.ts b/packages/workspace/src/Infra/MySQL/MySQLWorkspaceInviteRepository.ts index 9f722f134..d2dde8839 100644 --- a/packages/workspace/src/Infra/MySQL/MySQLWorkspaceInviteRepository.ts +++ b/packages/workspace/src/Infra/MySQL/MySQLWorkspaceInviteRepository.ts @@ -12,6 +12,10 @@ export class MySQLWorkspaceInviteRepository implements WorkspaceInviteRepository private ormRepository: Repository, ) {} + async findOneByUuid(uuid: string): Promise { + return this.ormRepository.createQueryBuilder().where('uuid = :uuid', { uuid }).getOne() + } + async save(workspaceInvite: WorkspaceInvite): Promise { return this.ormRepository.save(workspaceInvite) }