diff --git a/packages/api/src/Domain/Client/Workspace/WorkspaceApiOperations.ts b/packages/api/src/Domain/Client/Workspace/WorkspaceApiOperations.ts index 889c0dcab..57edfcecc 100644 --- a/packages/api/src/Domain/Client/Workspace/WorkspaceApiOperations.ts +++ b/packages/api/src/Domain/Client/Workspace/WorkspaceApiOperations.ts @@ -4,4 +4,5 @@ export enum WorkspaceApiOperations { Accepting, ListingWorkspaces, ListingWorkspaceUsers, + InitiatingKeyshare, } diff --git a/packages/api/src/Domain/Client/Workspace/WorkspaceApiService.spec.ts b/packages/api/src/Domain/Client/Workspace/WorkspaceApiService.spec.ts index d252aa3f8..87d9ec26a 100644 --- a/packages/api/src/Domain/Client/Workspace/WorkspaceApiService.spec.ts +++ b/packages/api/src/Domain/Client/Workspace/WorkspaceApiService.spec.ts @@ -1,4 +1,5 @@ import { WorkspaceAccessLevel, WorkspaceType } from '@standardnotes/common' + import { HttpStatusCode } from '../../Http' import { WorkspaceCreationResponse } from '../../Response/Workspace/WorkspaceCreationResponse' import { WorkspaceInvitationAcceptingResponse } from '../../Response/Workspace/WorkspaceInvitationAcceptingResponse' @@ -6,6 +7,7 @@ import { WorkspaceInvitationResponse } from '../../Response/Workspace/WorkspaceI import { WorkspaceListResponse } from '../../Response/Workspace/WorkspaceListResponse' import { WorkspaceUserListResponse } from '../../Response/Workspace/WorkspaceUserListResponse' import { WorkspaceServerInterface } from '../../Server/Workspace/WorkspaceServerInterface' +import { WorkspaceKeyshareInitiatingResponse } from '../../Response/Workspace/WorkspaceKeyshareInitiatingResponse' import { WorkspaceApiOperations } from './WorkspaceApiOperations' import { WorkspaceApiService } from './WorkspaceApiService' @@ -34,6 +36,10 @@ describe('WorkspaceApiService', () => { status: HttpStatusCode.Success, data: { users: [] }, } as jest.Mocked) + workspaceServer.initiateKeyshare = jest.fn().mockReturnValue({ + status: HttpStatusCode.Success, + data: { success: true }, + } as jest.Mocked) }) it('should create a workspace', async () => { @@ -304,4 +310,59 @@ describe('WorkspaceApiService', () => { expect(error).not.toBeNull() }) + + it('should initiate keyshare in workspace for user', async () => { + const response = await createService().initiateKeyshare({ + workspaceUuid: 'w-1-2-3', + userUuid: 'u-1-2-3', + encryptedWorkspaceKey: 'foobar', + }) + + expect(response).toEqual({ + status: 200, + data: { + success: true, + }, + }) + expect(workspaceServer.initiateKeyshare).toHaveBeenCalledWith({ + workspaceUuid: 'w-1-2-3', + userUuid: 'u-1-2-3', + encryptedWorkspaceKey: 'foobar', + }) + }) + + it('should not initiate keyshare in workspace if it is already initiating', async () => { + const service = createService() + Object.defineProperty(service, 'operationsInProgress', { + get: () => new Map([[WorkspaceApiOperations.InitiatingKeyshare, true]]), + }) + + let error = null + try { + await service.initiateKeyshare({ workspaceUuid: 'w-1-2-3', userUuid: 'u-1-2-3', encryptedWorkspaceKey: 'foobar' }) + } catch (caughtError) { + error = caughtError + } + + expect(error).not.toBeNull() + }) + + it('should not initiate keyshare in workspace if the server fails', async () => { + workspaceServer.initiateKeyshare = jest.fn().mockImplementation(() => { + throw new Error('Oops') + }) + + let error = null + try { + await createService().initiateKeyshare({ + workspaceUuid: 'w-1-2-3', + userUuid: 'u-1-2-3', + encryptedWorkspaceKey: 'foobar', + }) + } 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 41099857b..92f9f02cd 100644 --- a/packages/api/src/Domain/Client/Workspace/WorkspaceApiService.ts +++ b/packages/api/src/Domain/Client/Workspace/WorkspaceApiService.ts @@ -8,6 +8,7 @@ import { WorkspaceInvitationResponse } from '../../Response/Workspace/WorkspaceI import { WorkspaceServerInterface } from '../../Server/Workspace/WorkspaceServerInterface' import { WorkspaceListResponse } from '../../Response/Workspace/WorkspaceListResponse' import { WorkspaceUserListResponse } from '../../Response/Workspace/WorkspaceUserListResponse' +import { WorkspaceKeyshareInitiatingResponse } from '../../Response/Workspace/WorkspaceKeyshareInitiatingResponse' import { WorkspaceApiServiceInterface } from './WorkspaceApiServiceInterface' import { WorkspaceApiOperations } from './WorkspaceApiOperations' @@ -19,6 +20,28 @@ export class WorkspaceApiService implements WorkspaceApiServiceInterface { this.operationsInProgress = new Map() } + async initiateKeyshare(dto: { + workspaceUuid: string + userUuid: string + encryptedWorkspaceKey: string + }): Promise { + this.lockOperation(WorkspaceApiOperations.InitiatingKeyshare) + + try { + const response = await this.workspaceServer.initiateKeyshare({ + workspaceUuid: dto.workspaceUuid, + userUuid: dto.userUuid, + encryptedWorkspaceKey: dto.encryptedWorkspaceKey, + }) + + this.unlockOperation(WorkspaceApiOperations.InitiatingKeyshare) + + return response + } catch (error) { + throw new ApiCallError(ErrorMessage.GenericFail) + } + } + async listWorkspaceUsers(dto: { workspaceUuid: string }): Promise { this.lockOperation(WorkspaceApiOperations.ListingWorkspaceUsers) diff --git a/packages/api/src/Domain/Client/Workspace/WorkspaceApiServiceInterface.ts b/packages/api/src/Domain/Client/Workspace/WorkspaceApiServiceInterface.ts index c949ca6a1..fed57f388 100644 --- a/packages/api/src/Domain/Client/Workspace/WorkspaceApiServiceInterface.ts +++ b/packages/api/src/Domain/Client/Workspace/WorkspaceApiServiceInterface.ts @@ -1,5 +1,6 @@ import { Uuid, WorkspaceAccessLevel, WorkspaceType } from '@standardnotes/common' +import { WorkspaceKeyshareInitiatingResponse } from '../../Response/Workspace/WorkspaceKeyshareInitiatingResponse' import { WorkspaceCreationResponse } from '../../Response/Workspace/WorkspaceCreationResponse' import { WorkspaceInvitationAcceptingResponse } from '../../Response/Workspace/WorkspaceInvitationAcceptingResponse' import { WorkspaceInvitationResponse } from '../../Response/Workspace/WorkspaceInvitationResponse' @@ -27,4 +28,9 @@ export interface WorkspaceApiServiceInterface { }): Promise listWorkspaces(): Promise listWorkspaceUsers(dto: { workspaceUuid: Uuid }): Promise + initiateKeyshare(dto: { + workspaceUuid: Uuid + userUuid: Uuid + encryptedWorkspaceKey: string + }): Promise } diff --git a/packages/api/src/Domain/Request/Workspace/WorkspaceKeyshareInitiatingRequestParams.ts b/packages/api/src/Domain/Request/Workspace/WorkspaceKeyshareInitiatingRequestParams.ts new file mode 100644 index 000000000..786c43203 --- /dev/null +++ b/packages/api/src/Domain/Request/Workspace/WorkspaceKeyshareInitiatingRequestParams.ts @@ -0,0 +1,8 @@ +import { Uuid } from '@standardnotes/common' + +export type WorkspaceKeyshareInitiatingRequestParams = { + userUuid: Uuid + workspaceUuid: Uuid + encryptedWorkspaceKey: string + [additionalParam: string]: unknown +} diff --git a/packages/api/src/Domain/Request/index.ts b/packages/api/src/Domain/Request/index.ts index b0e53a01a..d6d491f82 100644 --- a/packages/api/src/Domain/Request/index.ts +++ b/packages/api/src/Domain/Request/index.ts @@ -9,5 +9,6 @@ export * from './WebSocket/WebSocketConnectionTokenRequestParams' export * from './Workspace/WorkspaceCreationRequestParams' export * from './Workspace/WorkspaceInvitationAcceptingRequestParams' export * from './Workspace/WorkspaceInvitationRequestParams' +export * from './Workspace/WorkspaceKeyshareInitiatingRequestParams' export * from './Workspace/WorkspaceListRequestParams' export * from './Workspace/WorkspaceUserListRequestParams' diff --git a/packages/api/src/Domain/Response/Workspace/WorkspaceKeyshareInitiatingResponse.ts b/packages/api/src/Domain/Response/Workspace/WorkspaceKeyshareInitiatingResponse.ts new file mode 100644 index 000000000..e29fc7046 --- /dev/null +++ b/packages/api/src/Domain/Response/Workspace/WorkspaceKeyshareInitiatingResponse.ts @@ -0,0 +1,9 @@ +import { Either } from '@standardnotes/common' + +import { HttpErrorResponseBody } from '../../Http/HttpErrorResponseBody' +import { HttpResponse } from '../../Http/HttpResponse' +import { WorkspaceKeyshareInitiatingResponseBody } from './WorkspaceKeyshareInitiatingResponseBody' + +export interface WorkspaceKeyshareInitiatingResponse extends HttpResponse { + data: Either +} diff --git a/packages/api/src/Domain/Response/Workspace/WorkspaceKeyshareInitiatingResponseBody.ts b/packages/api/src/Domain/Response/Workspace/WorkspaceKeyshareInitiatingResponseBody.ts new file mode 100644 index 000000000..649c43d14 --- /dev/null +++ b/packages/api/src/Domain/Response/Workspace/WorkspaceKeyshareInitiatingResponseBody.ts @@ -0,0 +1,3 @@ +export type WorkspaceKeyshareInitiatingResponseBody = { + success: boolean +} diff --git a/packages/api/src/Domain/Response/index.ts b/packages/api/src/Domain/Response/index.ts index b00245892..7bb95761a 100644 --- a/packages/api/src/Domain/Response/index.ts +++ b/packages/api/src/Domain/Response/index.ts @@ -16,7 +16,11 @@ export * from './Workspace/WorkspaceCreationResponse' export * from './Workspace/WorkspaceCreationResponseBody' export * from './Workspace/WorkspaceInvitationAcceptingResponse' export * from './Workspace/WorkspaceInvitationAcceptingResponseBody' +export * from './Workspace/WorkspaceKeyshareInitiatingResponse' +export * from './Workspace/WorkspaceKeyshareInitiatingResponseBody' export * from './Workspace/WorkspaceInvitationResponse' export * from './Workspace/WorkspaceInvitationResponseBody' export * from './Workspace/WorkspaceListResponse' export * from './Workspace/WorkspaceListResponseBody' +export * from './Workspace/WorkspaceUserListResponse' +export * from './Workspace/WorkspaceUserListResponseBody' diff --git a/packages/api/src/Domain/Server/Workspace/Paths.ts b/packages/api/src/Domain/Server/Workspace/Paths.ts index 59abb55f6..d9495e22f 100644 --- a/packages/api/src/Domain/Server/Workspace/Paths.ts +++ b/packages/api/src/Domain/Server/Workspace/Paths.ts @@ -4,6 +4,8 @@ const WorkspacePaths = { createWorkspace: '/v1/workspaces', listWorkspaces: '/v1/workspaces', listWorkspaceUsers: (uuid: Uuid) => `/v1/workspaces/${uuid}/users`, + initiateKeyshare: (worksapceUuid: Uuid, userUuid: Uuid) => + `/v1/workspaces/${worksapceUuid}/users/${userUuid}/keyshare`, inviteToWorkspace: (uuid: Uuid) => `/v1/workspaces/${uuid}/invites`, acceptInvite: (uuid: Uuid) => `/v1/invites/${uuid}/accept`, } diff --git a/packages/api/src/Domain/Server/Workspace/WorkspaceServer.spec.ts b/packages/api/src/Domain/Server/Workspace/WorkspaceServer.spec.ts index 0340a5891..b3632cf2a 100644 --- a/packages/api/src/Domain/Server/Workspace/WorkspaceServer.spec.ts +++ b/packages/api/src/Domain/Server/Workspace/WorkspaceServer.spec.ts @@ -1,8 +1,10 @@ import { WorkspaceAccessLevel, WorkspaceType } from '@standardnotes/common' + import { HttpServiceInterface, HttpStatusCode } from '../../Http' import { WorkspaceCreationResponse } from '../../Response/Workspace/WorkspaceCreationResponse' import { WorkspaceInvitationAcceptingResponse } from '../../Response/Workspace/WorkspaceInvitationAcceptingResponse' import { WorkspaceInvitationResponse } from '../../Response/Workspace/WorkspaceInvitationResponse' +import { WorkspaceKeyshareInitiatingResponse } from '../../Response/Workspace/WorkspaceKeyshareInitiatingResponse' import { WorkspaceListResponse } from '../../Response/Workspace/WorkspaceListResponse' import { WorkspaceUserListResponse } from '../../Response/Workspace/WorkspaceUserListResponse' @@ -99,4 +101,24 @@ describe('WorkspaceServer', () => { data: { users: [] }, }) }) + + it('should initiate keyshare for user in a workspace', async () => { + httpService.post = jest.fn().mockReturnValue({ + status: HttpStatusCode.Success, + data: { success: true }, + } as jest.Mocked) + + const response = await createServer().initiateKeyshare({ + workspaceUuid: 'w-1-2-3', + userUuid: 'u-1-2-3', + encryptedWorkspaceKey: 'foobar', + }) + + expect(response).toEqual({ + status: 200, + data: { + success: true, + }, + }) + }) }) diff --git a/packages/api/src/Domain/Server/Workspace/WorkspaceServer.ts b/packages/api/src/Domain/Server/Workspace/WorkspaceServer.ts index f82efcc06..30d1b2867 100644 --- a/packages/api/src/Domain/Server/Workspace/WorkspaceServer.ts +++ b/packages/api/src/Domain/Server/Workspace/WorkspaceServer.ts @@ -9,6 +9,8 @@ import { WorkspaceListRequestParams } from '../../Request/Workspace/WorkspaceLis import { WorkspaceListResponse } from '../../Response/Workspace/WorkspaceListResponse' import { WorkspaceUserListRequestParams } from '../../Request/Workspace/WorkspaceUserListRequestParams' import { WorkspaceUserListResponse } from '../../Response/Workspace/WorkspaceUserListResponse' +import { WorkspaceKeyshareInitiatingRequestParams } from '../../Request/Workspace/WorkspaceKeyshareInitiatingRequestParams' +import { WorkspaceKeyshareInitiatingResponse } from '../../Response/Workspace/WorkspaceKeyshareInitiatingResponse' import { Paths } from './Paths' import { WorkspaceServerInterface } from './WorkspaceServerInterface' @@ -16,6 +18,17 @@ import { WorkspaceServerInterface } from './WorkspaceServerInterface' export class WorkspaceServer implements WorkspaceServerInterface { constructor(private httpService: HttpServiceInterface) {} + async initiateKeyshare( + params: WorkspaceKeyshareInitiatingRequestParams, + ): Promise { + const response = await this.httpService.post( + Paths.v1.initiateKeyshare(params.workspaceUuid, params.userUuid), + params, + ) + + return response as WorkspaceKeyshareInitiatingResponse + } + async listWorkspaceUsers(params: WorkspaceUserListRequestParams): Promise { const response = await this.httpService.get(Paths.v1.listWorkspaceUsers(params.workspaceUuid), params) diff --git a/packages/api/src/Domain/Server/Workspace/WorkspaceServerInterface.ts b/packages/api/src/Domain/Server/Workspace/WorkspaceServerInterface.ts index 342879205..d39a83431 100644 --- a/packages/api/src/Domain/Server/Workspace/WorkspaceServerInterface.ts +++ b/packages/api/src/Domain/Server/Workspace/WorkspaceServerInterface.ts @@ -8,6 +8,8 @@ import { WorkspaceListRequestParams } from '../../Request/Workspace/WorkspaceLis import { WorkspaceListResponse } from '../../Response/Workspace/WorkspaceListResponse' import { WorkspaceUserListRequestParams } from '../../Request/Workspace/WorkspaceUserListRequestParams' import { WorkspaceUserListResponse } from '../../Response/Workspace/WorkspaceUserListResponse' +import { WorkspaceKeyshareInitiatingRequestParams } from '../../Request/Workspace/WorkspaceKeyshareInitiatingRequestParams' +import { WorkspaceKeyshareInitiatingResponse } from '../../Response/Workspace/WorkspaceKeyshareInitiatingResponse' export interface WorkspaceServerInterface { createWorkspace(params: WorkspaceCreationRequestParams): Promise @@ -15,4 +17,5 @@ export interface WorkspaceServerInterface { listWorkspaceUsers(params: WorkspaceUserListRequestParams): Promise inviteToWorkspace(params: WorkspaceInvitationRequestParams): Promise acceptInvite(params: WorkspaceInvitationAcceptingRequestParams): Promise + initiateKeyshare(params: WorkspaceKeyshareInitiatingRequestParams): Promise } diff --git a/packages/services/src/Domain/Workspace/WorkspaceClientInterface.ts b/packages/services/src/Domain/Workspace/WorkspaceClientInterface.ts index f6f86014d..9a4baefcc 100644 --- a/packages/services/src/Domain/Workspace/WorkspaceClientInterface.ts +++ b/packages/services/src/Domain/Workspace/WorkspaceClientInterface.ts @@ -22,4 +22,9 @@ export interface WorkspaceClientInterface { }): Promise<{ success: boolean }> listWorkspaces(): Promise<{ ownedWorkspaces: Array; joinedWorkspaces: Array }> listWorkspaceUsers(dto: { workspaceUuid: Uuid }): Promise<{ users: Array }> + initiateKeyshare(dto: { + workspaceUuid: Uuid + userUuid: Uuid + encryptedWorkspaceKey: string + }): Promise<{ success: boolean }> } diff --git a/packages/services/src/Domain/Workspace/WorkspaceManager.ts b/packages/services/src/Domain/Workspace/WorkspaceManager.ts index a62b9dae6..41a7180f9 100644 --- a/packages/services/src/Domain/Workspace/WorkspaceManager.ts +++ b/packages/services/src/Domain/Workspace/WorkspaceManager.ts @@ -14,6 +14,24 @@ export class WorkspaceManager extends AbstractService implements WorkspaceClient super(internalEventBus) } + async initiateKeyshare(dto: { + workspaceUuid: string + userUuid: string + encryptedWorkspaceKey: string + }): Promise<{ success: boolean }> { + try { + const result = await this.workspaceApiService.initiateKeyshare(dto) + + if (result.data.error !== undefined) { + return { success: false } + } + + return result.data + } catch (error) { + return { success: false } + } + } + async listWorkspaceUsers(dto: { workspaceUuid: string }): Promise<{ users: WorkspaceUser[] }> { try { const result = await this.workspaceApiService.listWorkspaceUsers(dto)