Skip to content

Commit

Permalink
feat(api): add accepting invites to workspace
Browse files Browse the repository at this point in the history
  • Loading branch information
Karol Sójko committed Oct 11, 2022
1 parent 1c22c1a commit 15e2c82
Show file tree
Hide file tree
Showing 15 changed files with 199 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export enum WorkspaceApiOperations {
Creating,
Inviting,
Accepting,
}
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -19,6 +20,9 @@ describe('WorkspaceApiService', () => {
workspaceServer.inviteToWorkspace = jest.fn().mockReturnValue({
data: { uuid: 'i-1-2-3' },
} as jest.Mocked<WorkspaceInvitationResponse>)
workspaceServer.acceptInvite = jest.fn().mockReturnValue({
data: { success: true },
} as jest.Mocked<WorkspaceInvitationAcceptingResponse>)
})

it('should create a workspace', async () => {
Expand Down Expand Up @@ -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()
})
})
54 changes: 41 additions & 13 deletions packages/api/src/Domain/Client/Workspace/WorkspaceApiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,54 @@ 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<WorkspaceApiOperations, boolean>

constructor(private workspaceServer: WorkspaceServerInterface) {
this.operationsInProgress = new Map()
}

async inviteToWorkspace(dto: { inviteeEmail: string; workspaceUuid: Uuid }): Promise<WorkspaceInvitationResponse> {
if (this.operationsInProgress.get(WorkspaceApiOperations.Inviting)) {
throw new ApiCallError(ErrorMessage.GenericInProgress)
async acceptInvite(dto: {
inviteUuid: string
userUuid: string
publicKey: string
encryptedPrivateKey: string
}): Promise<WorkspaceInvitationAcceptingResponse> {
this.lockOperation(WorkspaceApiOperations.Accepting)

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)
}
}

this.operationsInProgress.set(WorkspaceApiOperations.Inviting, true)
async inviteToWorkspace(dto: { inviteeEmail: string; workspaceUuid: Uuid }): Promise<WorkspaceInvitationResponse> {
this.lockOperation(WorkspaceApiOperations.Inviting)

try {
const response = await this.workspaceServer.inviteToWorkspace({
inviteeEmail: dto.inviteeEmail,
workspaceUuid: dto.workspaceUuid,
})

this.operationsInProgress.set(WorkspaceApiOperations.Inviting, false)
this.unlockOperation(WorkspaceApiOperations.Inviting)

return response
} catch (error) {
Expand All @@ -45,11 +65,7 @@ export class WorkspaceApiService implements WorkspaceApiServiceInterface {
publicKey?: string
workspaceName?: string
}): Promise<WorkspaceCreationResponse> {
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({
Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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: {
Expand All @@ -11,4 +13,10 @@ export interface WorkspaceApiServiceInterface {
workspaceName?: string
}): Promise<WorkspaceCreationResponse>
inviteToWorkspace(dto: { inviteeEmail: string; workspaceUuid: Uuid }): Promise<WorkspaceInvitationResponse>
acceptInvite(dto: {
inviteUuid: Uuid
userUuid: Uuid
publicKey: string
encryptedPrivateKey: string
}): Promise<WorkspaceInvitationAcceptingResponse>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Uuid } from '@standardnotes/common'

export type WorkspaceInvitationAcceptingRequestParams = {
inviteUuid: Uuid
userUuid: Uuid
publicKey: string
encryptedPrivateKey: string
[additionalParam: string]: unknown
}
1 change: 1 addition & 0 deletions packages/api/src/Domain/Request/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Original file line number Diff line number Diff line change
@@ -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<WorkspaceInvitationAcceptingResponseBody, HttpErrorResponseBody>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type WorkspaceInvitationAcceptingResponseBody = {
success: boolean
}
2 changes: 2 additions & 0 deletions packages/api/src/Domain/Response/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
1 change: 1 addition & 0 deletions packages/api/src/Domain/Server/Workspace/Paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
20 changes: 20 additions & 0 deletions packages/api/src/Domain/Server/Workspace/WorkspaceServer.spec.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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<WorkspaceInvitationAcceptingResponse>)

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,
},
})
})
})
8 changes: 8 additions & 0 deletions packages/api/src/Domain/Server/Workspace/WorkspaceServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,21 @@ 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'

export class WorkspaceServer implements WorkspaceServerInterface {
constructor(private httpService: HttpServiceInterface) {}

async acceptInvite(params: WorkspaceInvitationAcceptingRequestParams): Promise<WorkspaceInvitationAcceptingResponse> {
const response = await this.httpService.post(Paths.v1.acceptInvite(params.inviteUuid), params)

return response as WorkspaceInvitationAcceptingResponse
}

async inviteToWorkspace(params: WorkspaceInvitationRequestParams): Promise<WorkspaceInvitationResponse> {
const response = await this.httpService.post(Paths.v1.inviteToWorkspace(params.workspaceUuid), params)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<WorkspaceCreationResponse>
inviteToWorkspace(params: WorkspaceInvitationRequestParams): Promise<WorkspaceInvitationResponse>
acceptInvite(params: WorkspaceInvitationAcceptingRequestParams): Promise<WorkspaceInvitationAcceptingResponse>
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 }>
}
19 changes: 19 additions & 0 deletions packages/services/src/Domain/Workspace/WorkspaceManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 15e2c82

Please sign in to comment.