From 15e2c82e654ce3f983cd42b01f14d18435b3e939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20S=C3=B3jko?= Date: Tue, 11 Oct 2022 10:33:23 +0200 Subject: [PATCH] feat(api): add accepting invites to workspace --- .../Workspace/WorkspaceApiOperations.ts | 1 + .../Workspace/WorkspaceApiService.spec.ts | 68 ++++++++++++++++++- .../Client/Workspace/WorkspaceApiService.ts | 56 +++++++++++---- .../Workspace/WorkspaceApiServiceInterface.ts | 10 ++- ...rkspaceInvitationAcceptingRequestParams.ts | 9 +++ packages/api/src/Domain/Request/index.ts | 1 + .../WorkspaceInvitationAcceptingResponse.ts | 9 +++ ...orkspaceInvitationAcceptingResponseBody.ts | 3 + packages/api/src/Domain/Response/index.ts | 2 + .../api/src/Domain/Server/Workspace/Paths.ts | 1 + .../Server/Workspace/WorkspaceServer.spec.ts | 20 ++++++ .../Server/Workspace/WorkspaceServer.ts | 8 +++ .../Workspace/WorkspaceServerInterface.ts | 3 + .../Workspace/WorkspaceClientInterface.ts | 6 ++ .../src/Domain/Workspace/WorkspaceManager.ts | 19 ++++++ 15 files changed, 200 insertions(+), 16 deletions(-) create mode 100644 packages/api/src/Domain/Request/Workspace/WorkspaceInvitationAcceptingRequestParams.ts create mode 100644 packages/api/src/Domain/Response/Workspace/WorkspaceInvitationAcceptingResponse.ts create mode 100644 packages/api/src/Domain/Response/Workspace/WorkspaceInvitationAcceptingResponseBody.ts diff --git a/packages/api/src/Domain/Client/Workspace/WorkspaceApiOperations.ts b/packages/api/src/Domain/Client/Workspace/WorkspaceApiOperations.ts index 3f100c4d7..d3fb6acc9 100644 --- a/packages/api/src/Domain/Client/Workspace/WorkspaceApiOperations.ts +++ b/packages/api/src/Domain/Client/Workspace/WorkspaceApiOperations.ts @@ -1,4 +1,5 @@ export enum WorkspaceApiOperations { Creating, Inviting, + Accepting, } diff --git a/packages/api/src/Domain/Client/Workspace/WorkspaceApiService.spec.ts b/packages/api/src/Domain/Client/Workspace/WorkspaceApiService.spec.ts index 08872d9d2..e21301cd1 100644 --- a/packages/api/src/Domain/Client/Workspace/WorkspaceApiService.spec.ts +++ b/packages/api/src/Domain/Client/Workspace/WorkspaceApiService.spec.ts @@ -1,6 +1,7 @@ import { WorkspaceType } from '@standardnotes/common' -import { WorkspaceInvitationResponse } from '../../Response' import { WorkspaceCreationResponse } from '../../Response/Workspace/WorkspaceCreationResponse' +import { WorkspaceInvitationAcceptingResponse } from '../../Response/Workspace/WorkspaceInvitationAcceptingResponse' +import { WorkspaceInvitationResponse } from '../../Response/Workspace/WorkspaceInvitationResponse' import { WorkspaceServerInterface } from '../../Server/Workspace/WorkspaceServerInterface' import { WorkspaceApiOperations } from './WorkspaceApiOperations' @@ -19,6 +20,9 @@ describe('WorkspaceApiService', () => { workspaceServer.inviteToWorkspace = jest.fn().mockReturnValue({ data: { uuid: 'i-1-2-3' }, } as jest.Mocked) + workspaceServer.acceptInvite = jest.fn().mockReturnValue({ + data: { success: true }, + } as jest.Mocked) }) it('should create a workspace', async () => { @@ -136,4 +140,66 @@ describe('WorkspaceApiService', () => { expect(error).not.toBeNull() }) + + it('should accept invite to a workspace', async () => { + const response = await createService().acceptInvite({ + userUuid: 'u-1-2-3', + inviteUuid: 'i-1-2-3', + publicKey: 'foo', + encryptedPrivateKey: 'bar', + }) + + expect(response).toEqual({ + data: { + success: true, + }, + }) + expect(workspaceServer.acceptInvite).toHaveBeenCalledWith({ + userUuid: 'u-1-2-3', + inviteUuid: 'i-1-2-3', + publicKey: 'foo', + encryptedPrivateKey: 'bar', + }) + }) + + it('should not accept invite to a workspace if it is already accepting', async () => { + const service = createService() + Object.defineProperty(service, 'operationsInProgress', { + get: () => new Map([[WorkspaceApiOperations.Accepting, true]]), + }) + + let error = null + try { + await service.acceptInvite({ + userUuid: 'u-1-2-3', + inviteUuid: 'i-1-2-3', + publicKey: 'foo', + encryptedPrivateKey: 'bar', + }) + } catch (caughtError) { + error = caughtError + } + + expect(error).not.toBeNull() + }) + + it('should not accept invite to a workspace if the server fails', async () => { + workspaceServer.acceptInvite = jest.fn().mockImplementation(() => { + throw new Error('Oops') + }) + + let error = null + try { + await createService().acceptInvite({ + userUuid: 'u-1-2-3', + inviteUuid: 'i-1-2-3', + publicKey: 'foo', + encryptedPrivateKey: 'bar', + }) + } catch (caughtError) { + error = caughtError + } + + expect(error).not.toBeNull() + }) }) diff --git a/packages/api/src/Domain/Client/Workspace/WorkspaceApiService.ts b/packages/api/src/Domain/Client/Workspace/WorkspaceApiService.ts index 87d71ce9d..cef2ffa2e 100644 --- a/packages/api/src/Domain/Client/Workspace/WorkspaceApiService.ts +++ b/packages/api/src/Domain/Client/Workspace/WorkspaceApiService.ts @@ -3,13 +3,13 @@ import { Uuid, WorkspaceType } from '@standardnotes/common' import { ErrorMessage } from '../../Error/ErrorMessage' import { ApiCallError } from '../../Error/ApiCallError' import { WorkspaceCreationResponse } from '../../Response/Workspace/WorkspaceCreationResponse' +import { WorkspaceInvitationAcceptingResponse } from '../../Response/Workspace/WorkspaceInvitationAcceptingResponse' +import { WorkspaceInvitationResponse } from '../../Response/Workspace/WorkspaceInvitationResponse' import { WorkspaceServerInterface } from '../../Server/Workspace/WorkspaceServerInterface' import { WorkspaceApiServiceInterface } from './WorkspaceApiServiceInterface' import { WorkspaceApiOperations } from './WorkspaceApiOperations' -import { WorkspaceInvitationResponse } from '../../Response' - export class WorkspaceApiService implements WorkspaceApiServiceInterface { private operationsInProgress: Map @@ -17,12 +17,32 @@ export class WorkspaceApiService implements WorkspaceApiServiceInterface { this.operationsInProgress = new Map() } - async inviteToWorkspace(dto: { inviteeEmail: string; workspaceUuid: Uuid }): Promise { - if (this.operationsInProgress.get(WorkspaceApiOperations.Inviting)) { - throw new ApiCallError(ErrorMessage.GenericInProgress) - } + async acceptInvite(dto: { + inviteUuid: string + userUuid: string + publicKey: string + encryptedPrivateKey: string + }): Promise { + this.lockOperation(WorkspaceApiOperations.Accepting) - this.operationsInProgress.set(WorkspaceApiOperations.Inviting, true) + try { + const response = await this.workspaceServer.acceptInvite({ + encryptedPrivateKey: dto.encryptedPrivateKey, + publicKey: dto.publicKey, + inviteUuid: dto.inviteUuid, + userUuid: dto.userUuid, + }) + + this.unlockOperation(WorkspaceApiOperations.Accepting) + + return response + } catch (error) { + throw new ApiCallError(ErrorMessage.GenericFail) + } + } + + async inviteToWorkspace(dto: { inviteeEmail: string; workspaceUuid: Uuid }): Promise { + this.lockOperation(WorkspaceApiOperations.Inviting) try { const response = await this.workspaceServer.inviteToWorkspace({ @@ -30,7 +50,7 @@ export class WorkspaceApiService implements WorkspaceApiServiceInterface { workspaceUuid: dto.workspaceUuid, }) - this.operationsInProgress.set(WorkspaceApiOperations.Inviting, false) + this.unlockOperation(WorkspaceApiOperations.Inviting) return response } catch (error) { @@ -45,11 +65,7 @@ export class WorkspaceApiService implements WorkspaceApiServiceInterface { publicKey?: string workspaceName?: string }): Promise { - if (this.operationsInProgress.get(WorkspaceApiOperations.Creating)) { - throw new ApiCallError(ErrorMessage.GenericInProgress) - } - - this.operationsInProgress.set(WorkspaceApiOperations.Creating, true) + this.lockOperation(WorkspaceApiOperations.Creating) try { const response = await this.workspaceServer.createWorkspace({ @@ -60,11 +76,23 @@ export class WorkspaceApiService implements WorkspaceApiServiceInterface { workspaceName: dto.workspaceName, }) - this.operationsInProgress.set(WorkspaceApiOperations.Creating, false) + this.unlockOperation(WorkspaceApiOperations.Creating) return response } catch (error) { throw new ApiCallError(ErrorMessage.GenericFail) } } + + private lockOperation(operation: WorkspaceApiOperations): void { + if (this.operationsInProgress.get(operation)) { + throw new ApiCallError(ErrorMessage.GenericInProgress) + } + + this.operationsInProgress.set(operation, true) + } + + private unlockOperation(operation: WorkspaceApiOperations): void { + this.operationsInProgress.set(operation, false) + } } diff --git a/packages/api/src/Domain/Client/Workspace/WorkspaceApiServiceInterface.ts b/packages/api/src/Domain/Client/Workspace/WorkspaceApiServiceInterface.ts index a296bd6d4..ff7cbc52d 100644 --- a/packages/api/src/Domain/Client/Workspace/WorkspaceApiServiceInterface.ts +++ b/packages/api/src/Domain/Client/Workspace/WorkspaceApiServiceInterface.ts @@ -1,6 +1,8 @@ import { Uuid, WorkspaceType } from '@standardnotes/common' -import { WorkspaceCreationResponse, WorkspaceInvitationResponse } from '../../Response' +import { WorkspaceCreationResponse } from '../../Response/Workspace/WorkspaceCreationResponse' +import { WorkspaceInvitationAcceptingResponse } from '../../Response/Workspace/WorkspaceInvitationAcceptingResponse' +import { WorkspaceInvitationResponse } from '../../Response/Workspace/WorkspaceInvitationResponse' export interface WorkspaceApiServiceInterface { createWorkspace(dto: { @@ -11,4 +13,10 @@ export interface WorkspaceApiServiceInterface { workspaceName?: string }): Promise inviteToWorkspace(dto: { inviteeEmail: string; workspaceUuid: Uuid }): Promise + acceptInvite(dto: { + inviteUuid: Uuid + userUuid: Uuid + publicKey: string + encryptedPrivateKey: string + }): Promise } diff --git a/packages/api/src/Domain/Request/Workspace/WorkspaceInvitationAcceptingRequestParams.ts b/packages/api/src/Domain/Request/Workspace/WorkspaceInvitationAcceptingRequestParams.ts new file mode 100644 index 000000000..4349e6708 --- /dev/null +++ b/packages/api/src/Domain/Request/Workspace/WorkspaceInvitationAcceptingRequestParams.ts @@ -0,0 +1,9 @@ +import { Uuid } from '@standardnotes/common' + +export type WorkspaceInvitationAcceptingRequestParams = { + inviteUuid: Uuid + userUuid: Uuid + publicKey: string + encryptedPrivateKey: string + [additionalParam: string]: unknown +} diff --git a/packages/api/src/Domain/Request/index.ts b/packages/api/src/Domain/Request/index.ts index 253463f0e..b079056ea 100644 --- a/packages/api/src/Domain/Request/index.ts +++ b/packages/api/src/Domain/Request/index.ts @@ -7,4 +7,5 @@ export * from './Subscription/SubscriptionInviteRequestParams' export * from './User/UserRegistrationRequestParams' export * from './WebSocket/WebSocketConnectionTokenRequestParams' export * from './Workspace/WorkspaceCreationRequestParams' +export * from './Workspace/WorkspaceInvitationAcceptingRequestParams' export * from './Workspace/WorkspaceInvitationRequestParams' diff --git a/packages/api/src/Domain/Response/Workspace/WorkspaceInvitationAcceptingResponse.ts b/packages/api/src/Domain/Response/Workspace/WorkspaceInvitationAcceptingResponse.ts new file mode 100644 index 000000000..04e4d583b --- /dev/null +++ b/packages/api/src/Domain/Response/Workspace/WorkspaceInvitationAcceptingResponse.ts @@ -0,0 +1,9 @@ +import { Either } from '@standardnotes/common' + +import { HttpErrorResponseBody } from '../../Http/HttpErrorResponseBody' +import { HttpResponse } from '../../Http/HttpResponse' +import { WorkspaceInvitationAcceptingResponseBody } from './WorkspaceInvitationAcceptingResponseBody' + +export interface WorkspaceInvitationAcceptingResponse extends HttpResponse { + data: Either +} diff --git a/packages/api/src/Domain/Response/Workspace/WorkspaceInvitationAcceptingResponseBody.ts b/packages/api/src/Domain/Response/Workspace/WorkspaceInvitationAcceptingResponseBody.ts new file mode 100644 index 000000000..cf3dcf842 --- /dev/null +++ b/packages/api/src/Domain/Response/Workspace/WorkspaceInvitationAcceptingResponseBody.ts @@ -0,0 +1,3 @@ +export type WorkspaceInvitationAcceptingResponseBody = { + success: boolean +} diff --git a/packages/api/src/Domain/Response/index.ts b/packages/api/src/Domain/Response/index.ts index f2b3dbbee..8de8d117e 100644 --- a/packages/api/src/Domain/Response/index.ts +++ b/packages/api/src/Domain/Response/index.ts @@ -14,5 +14,7 @@ export * from './WebSocket/WebSocketConnectionTokenResponse' export * from './WebSocket/WebSocketConnectionTokenResponseBody' export * from './Workspace/WorkspaceCreationResponse' export * from './Workspace/WorkspaceCreationResponseBody' +export * from './Workspace/WorkspaceInvitationAcceptingResponse' +export * from './Workspace/WorkspaceInvitationAcceptingResponseBody' export * from './Workspace/WorkspaceInvitationResponse' export * from './Workspace/WorkspaceInvitationResponseBody' diff --git a/packages/api/src/Domain/Server/Workspace/Paths.ts b/packages/api/src/Domain/Server/Workspace/Paths.ts index 15314dc30..ca9b42d94 100644 --- a/packages/api/src/Domain/Server/Workspace/Paths.ts +++ b/packages/api/src/Domain/Server/Workspace/Paths.ts @@ -3,6 +3,7 @@ import { Uuid } from '@standardnotes/common' const WorkspacePaths = { createWorkspace: '/v1/workspaces', inviteToWorkspace: (uuid: Uuid) => `/v1/workspaces/${uuid}/invites`, + acceptInvite: (uuid: Uuid) => `/v1/invites/${uuid}/accept`, } export const Paths = { diff --git a/packages/api/src/Domain/Server/Workspace/WorkspaceServer.spec.ts b/packages/api/src/Domain/Server/Workspace/WorkspaceServer.spec.ts index c188fdf23..6f12e023d 100644 --- a/packages/api/src/Domain/Server/Workspace/WorkspaceServer.spec.ts +++ b/packages/api/src/Domain/Server/Workspace/WorkspaceServer.spec.ts @@ -1,6 +1,7 @@ import { WorkspaceType } from '@standardnotes/common' import { HttpServiceInterface } from '../../Http' import { WorkspaceCreationResponse } from '../../Response/Workspace/WorkspaceCreationResponse' +import { WorkspaceInvitationAcceptingResponse } from '../../Response/Workspace/WorkspaceInvitationAcceptingResponse' import { WorkspaceInvitationResponse } from '../../Response/Workspace/WorkspaceInvitationResponse' import { WorkspaceServer } from './WorkspaceServer' @@ -48,4 +49,23 @@ describe('WorkspaceServer', () => { }, }) }) + + it('should accept invitation to a workspace', async () => { + httpService.post = jest.fn().mockReturnValue({ + data: { success: true }, + } as jest.Mocked) + + const response = await createServer().acceptInvite({ + encryptedPrivateKey: 'foo', + inviteUuid: 'i-1-2-3', + publicKey: 'bar', + userUuid: 'u-1-2-3', + }) + + expect(response).toEqual({ + data: { + success: true, + }, + }) + }) }) diff --git a/packages/api/src/Domain/Server/Workspace/WorkspaceServer.ts b/packages/api/src/Domain/Server/Workspace/WorkspaceServer.ts index 0e9dcb1ae..fc0afa013 100644 --- a/packages/api/src/Domain/Server/Workspace/WorkspaceServer.ts +++ b/packages/api/src/Domain/Server/Workspace/WorkspaceServer.ts @@ -3,6 +3,8 @@ import { WorkspaceInvitationRequestParams } from '../../Request/Workspace/Worksp import { WorkspaceCreationRequestParams } from '../../Request/Workspace/WorkspaceCreationRequestParams' import { WorkspaceInvitationResponse } from '../../Response/Workspace/WorkspaceInvitationResponse' import { WorkspaceCreationResponse } from '../../Response/Workspace/WorkspaceCreationResponse' +import { WorkspaceInvitationAcceptingRequestParams } from '../../Request/Workspace/WorkspaceInvitationAcceptingRequestParams' +import { WorkspaceInvitationAcceptingResponse } from '../../Response/Workspace/WorkspaceInvitationAcceptingResponse' import { Paths } from './Paths' import { WorkspaceServerInterface } from './WorkspaceServerInterface' @@ -10,6 +12,12 @@ import { WorkspaceServerInterface } from './WorkspaceServerInterface' export class WorkspaceServer implements WorkspaceServerInterface { constructor(private httpService: HttpServiceInterface) {} + async acceptInvite(params: WorkspaceInvitationAcceptingRequestParams): Promise { + const response = await this.httpService.post(Paths.v1.acceptInvite(params.inviteUuid), params) + + return response as WorkspaceInvitationAcceptingResponse + } + async inviteToWorkspace(params: WorkspaceInvitationRequestParams): Promise { const response = await this.httpService.post(Paths.v1.inviteToWorkspace(params.workspaceUuid), params) diff --git a/packages/api/src/Domain/Server/Workspace/WorkspaceServerInterface.ts b/packages/api/src/Domain/Server/Workspace/WorkspaceServerInterface.ts index d29e44373..f765de2c0 100644 --- a/packages/api/src/Domain/Server/Workspace/WorkspaceServerInterface.ts +++ b/packages/api/src/Domain/Server/Workspace/WorkspaceServerInterface.ts @@ -2,8 +2,11 @@ import { WorkspaceInvitationRequestParams } from '../../Request/Workspace/Worksp import { WorkspaceCreationRequestParams } from '../../Request/Workspace/WorkspaceCreationRequestParams' import { WorkspaceInvitationResponse } from '../../Response/Workspace/WorkspaceInvitationResponse' import { WorkspaceCreationResponse } from '../../Response/Workspace/WorkspaceCreationResponse' +import { WorkspaceInvitationAcceptingRequestParams } from '../../Request/Workspace/WorkspaceInvitationAcceptingRequestParams' +import { WorkspaceInvitationAcceptingResponse } from '../../Response/Workspace/WorkspaceInvitationAcceptingResponse' export interface WorkspaceServerInterface { createWorkspace(params: WorkspaceCreationRequestParams): Promise inviteToWorkspace(params: WorkspaceInvitationRequestParams): Promise + acceptInvite(params: WorkspaceInvitationAcceptingRequestParams): Promise } diff --git a/packages/services/src/Domain/Workspace/WorkspaceClientInterface.ts b/packages/services/src/Domain/Workspace/WorkspaceClientInterface.ts index 861770f74..573c1bc44 100644 --- a/packages/services/src/Domain/Workspace/WorkspaceClientInterface.ts +++ b/packages/services/src/Domain/Workspace/WorkspaceClientInterface.ts @@ -9,4 +9,10 @@ export interface WorkspaceClientInterface { workspaceName?: string }): Promise<{ uuid: string } | null> inviteToWorkspace(dto: { inviteeEmail: string; workspaceUuid: Uuid }): Promise<{ uuid: string } | null> + acceptInvite(dto: { + inviteUuid: Uuid + userUuid: Uuid + publicKey: string + encryptedPrivateKey: string + }): Promise<{ success: boolean }> } diff --git a/packages/services/src/Domain/Workspace/WorkspaceManager.ts b/packages/services/src/Domain/Workspace/WorkspaceManager.ts index 8be2fbd67..d18504b14 100644 --- a/packages/services/src/Domain/Workspace/WorkspaceManager.ts +++ b/packages/services/src/Domain/Workspace/WorkspaceManager.ts @@ -13,6 +13,25 @@ export class WorkspaceManager extends AbstractService implements WorkspaceClient super(internalEventBus) } + async acceptInvite(dto: { + inviteUuid: string + userUuid: string + publicKey: string + encryptedPrivateKey: string + }): Promise<{ success: boolean }> { + try { + const result = await this.workspaceApiService.acceptInvite(dto) + + if (result.data.error !== undefined) { + return { success: false } + } + + return result.data + } catch (error) { + return { success: false } + } + } + async inviteToWorkspace(dto: { inviteeEmail: string; workspaceUuid: Uuid }): Promise<{ uuid: string } | null> { try { const result = await this.workspaceApiService.inviteToWorkspace(dto)