diff --git a/.pnp.cjs b/.pnp.cjs index 6613566f9..9d125c881 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -2521,16 +2521,16 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }]\ ]],\ ["@standardnotes/api", [\ - ["npm:1.12.1", {\ - "packageLocation": "./.yarn/cache/@standardnotes-api-npm-1.12.1-6b8bfe4ccf-8623fc82de.zip/node_modules/@standardnotes/api/",\ + ["npm:1.15.0", {\ + "packageLocation": "./.yarn/cache/@standardnotes-api-npm-1.15.0-12a67ff9b7-88ae0a340e.zip/node_modules/@standardnotes/api/",\ "packageDependencies": [\ - ["@standardnotes/api", "npm:1.12.1"],\ - ["@standardnotes/common", "workspace:packages/common"],\ - ["@standardnotes/encryption", "npm:1.16.2"],\ - ["@standardnotes/models", "npm:1.24.2"],\ - ["@standardnotes/responses", "npm:1.10.6"],\ + ["@standardnotes/api", "npm:1.15.0"],\ + ["@standardnotes/common", "npm:1.39.0"],\ + ["@standardnotes/encryption", "npm:1.17.0"],\ + ["@standardnotes/models", "npm:1.26.0"],\ + ["@standardnotes/responses", "npm:1.11.0"],\ ["@standardnotes/security", "workspace:packages/security"],\ - ["@standardnotes/utils", "npm:1.9.1"],\ + ["@standardnotes/utils", "npm:1.10.0"],\ ["reflect-metadata", "npm:0.1.13"]\ ],\ "linkType": "HARD"\ @@ -2600,7 +2600,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@newrelic/winston-enricher", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:4.0.0"],\ ["@sentry/node", "npm:7.5.0"],\ ["@standardnotes/analytics", "workspace:packages/analytics"],\ - ["@standardnotes/api", "npm:1.12.1"],\ + ["@standardnotes/api", "npm:1.15.0"],\ ["@standardnotes/common", "workspace:packages/common"],\ ["@standardnotes/domain-events", "workspace:packages/domain-events"],\ ["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\ @@ -2653,6 +2653,14 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }]\ ]],\ ["@standardnotes/common", [\ + ["npm:1.39.0", {\ + "packageLocation": "./.yarn/cache/@standardnotes-common-npm-1.39.0-1e36e2ef74-92cfad04a6.zip/node_modules/@standardnotes/common/",\ + "packageDependencies": [\ + ["@standardnotes/common", "npm:1.39.0"],\ + ["reflect-metadata", "npm:0.1.13"]\ + ],\ + "linkType": "HARD"\ + }],\ ["workspace:packages/common", {\ "packageLocation": "./packages/common/",\ "packageDependencies": [\ @@ -2725,15 +2733,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }]\ ]],\ ["@standardnotes/encryption", [\ - ["npm:1.16.2", {\ - "packageLocation": "./.yarn/cache/@standardnotes-encryption-npm-1.16.2-9e53125abe-50efc1b201.zip/node_modules/@standardnotes/encryption/",\ + ["npm:1.17.0", {\ + "packageLocation": "./.yarn/cache/@standardnotes-encryption-npm-1.17.0-587d631df2-587516dfed.zip/node_modules/@standardnotes/encryption/",\ "packageDependencies": [\ - ["@standardnotes/encryption", "npm:1.16.2"],\ - ["@standardnotes/common", "workspace:packages/common"],\ - ["@standardnotes/models", "npm:1.24.2"],\ - ["@standardnotes/responses", "npm:1.10.6"],\ + ["@standardnotes/encryption", "npm:1.17.0"],\ + ["@standardnotes/common", "npm:1.39.0"],\ + ["@standardnotes/models", "npm:1.26.0"],\ + ["@standardnotes/responses", "npm:1.11.0"],\ ["@standardnotes/sncrypto-common", "npm:1.13.0"],\ - ["@standardnotes/utils", "npm:1.9.1"],\ + ["@standardnotes/utils", "npm:1.10.0"],\ ["reflect-metadata", "npm:0.1.13"]\ ],\ "linkType": "HARD"\ @@ -2791,12 +2799,12 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ],\ "linkType": "HARD"\ }],\ - ["npm:1.52.4", {\ - "packageLocation": "./.yarn/cache/@standardnotes-features-npm-1.52.4-05c59084e4-aea7b48627.zip/node_modules/@standardnotes/features/",\ + ["npm:1.53.0", {\ + "packageLocation": "./.yarn/cache/@standardnotes-features-npm-1.53.0-8ea4a2d559-a856e815a3.zip/node_modules/@standardnotes/features/",\ "packageDependencies": [\ - ["@standardnotes/features", "npm:1.52.4"],\ + ["@standardnotes/features", "npm:1.53.0"],\ ["@standardnotes/auth", "npm:3.19.4"],\ - ["@standardnotes/common", "workspace:packages/common"],\ + ["@standardnotes/common", "npm:1.39.0"],\ ["@standardnotes/security", "workspace:packages/security"],\ ["reflect-metadata", "npm:0.1.13"]\ ],\ @@ -2856,14 +2864,14 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }]\ ]],\ ["@standardnotes/models", [\ - ["npm:1.24.2", {\ - "packageLocation": "./.yarn/cache/@standardnotes-models-npm-1.24.2-8c2c157efa-17b3cfba39.zip/node_modules/@standardnotes/models/",\ + ["npm:1.26.0", {\ + "packageLocation": "./.yarn/cache/@standardnotes-models-npm-1.26.0-dade8919ab-f595a3de88.zip/node_modules/@standardnotes/models/",\ "packageDependencies": [\ - ["@standardnotes/models", "npm:1.24.2"],\ - ["@standardnotes/common", "workspace:packages/common"],\ - ["@standardnotes/features", "npm:1.52.4"],\ - ["@standardnotes/responses", "npm:1.10.6"],\ - ["@standardnotes/utils", "npm:1.9.1"],\ + ["@standardnotes/models", "npm:1.26.0"],\ + ["@standardnotes/common", "npm:1.39.0"],\ + ["@standardnotes/features", "npm:1.53.0"],\ + ["@standardnotes/responses", "npm:1.11.0"],\ + ["@standardnotes/utils", "npm:1.10.0"],\ ["lodash", "npm:4.17.21"],\ ["reflect-metadata", "npm:0.1.13"]\ ],\ @@ -2899,12 +2907,12 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }]\ ]],\ ["@standardnotes/responses", [\ - ["npm:1.10.6", {\ - "packageLocation": "./.yarn/cache/@standardnotes-responses-npm-1.10.6-f636794f47-0583e2cb77.zip/node_modules/@standardnotes/responses/",\ + ["npm:1.11.0", {\ + "packageLocation": "./.yarn/cache/@standardnotes-responses-npm-1.11.0-d066ddbbb6-46d6a47980.zip/node_modules/@standardnotes/responses/",\ "packageDependencies": [\ - ["@standardnotes/responses", "npm:1.10.6"],\ - ["@standardnotes/common", "workspace:packages/common"],\ - ["@standardnotes/features", "npm:1.52.4"],\ + ["@standardnotes/responses", "npm:1.11.0"],\ + ["@standardnotes/common", "npm:1.39.0"],\ + ["@standardnotes/features", "npm:1.53.0"],\ ["@standardnotes/security", "workspace:packages/security"],\ ["reflect-metadata", "npm:0.1.13"]\ ],\ @@ -3125,6 +3133,17 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }]\ ]],\ ["@standardnotes/utils", [\ + ["npm:1.10.0", {\ + "packageLocation": "./.yarn/cache/@standardnotes-utils-npm-1.10.0-0dc2ade40b-c02d54ca8a.zip/node_modules/@standardnotes/utils/",\ + "packageDependencies": [\ + ["@standardnotes/utils", "npm:1.10.0"],\ + ["@standardnotes/common", "npm:1.39.0"],\ + ["dompurify", "npm:2.4.0"],\ + ["lodash", "npm:4.17.21"],\ + ["reflect-metadata", "npm:0.1.13"]\ + ],\ + "linkType": "HARD"\ + }],\ ["npm:1.6.12", {\ "packageLocation": "./.yarn/cache/@standardnotes-utils-npm-1.6.12-8fa8d7d09b-e177b1fa51.zip/node_modules/@standardnotes/utils/",\ "packageDependencies": [\ @@ -3134,17 +3153,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["lodash", "npm:4.17.21"]\ ],\ "linkType": "HARD"\ - }],\ - ["npm:1.9.1", {\ - "packageLocation": "./.yarn/cache/@standardnotes-utils-npm-1.9.1-e48d87ffc7-f775bb3744.zip/node_modules/@standardnotes/utils/",\ - "packageDependencies": [\ - ["@standardnotes/utils", "npm:1.9.1"],\ - ["@standardnotes/common", "workspace:packages/common"],\ - ["dompurify", "npm:2.4.0"],\ - ["lodash", "npm:4.17.21"],\ - ["reflect-metadata", "npm:0.1.13"]\ - ],\ - "linkType": "HARD"\ }]\ ]],\ ["@standardnotes/workspace-server", [\ @@ -3154,10 +3162,11 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@standardnotes/workspace-server", "workspace:packages/workspace"],\ ["@newrelic/winston-enricher", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:4.0.0"],\ ["@sentry/node", "npm:7.5.0"],\ - ["@standardnotes/api", "npm:1.12.1"],\ + ["@standardnotes/api", "npm:1.15.0"],\ ["@standardnotes/common", "workspace:packages/common"],\ ["@standardnotes/domain-events", "workspace:packages/domain-events"],\ ["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\ + ["@standardnotes/models", "npm:1.26.0"],\ ["@standardnotes/security", "workspace:packages/security"],\ ["@standardnotes/time", "workspace:packages/time"],\ ["@types/cors", "npm:2.8.12"],\ diff --git a/.yarn/cache/@standardnotes-api-npm-1.12.1-6b8bfe4ccf-8623fc82de.zip b/.yarn/cache/@standardnotes-api-npm-1.15.0-12a67ff9b7-88ae0a340e.zip similarity index 66% rename from .yarn/cache/@standardnotes-api-npm-1.12.1-6b8bfe4ccf-8623fc82de.zip rename to .yarn/cache/@standardnotes-api-npm-1.15.0-12a67ff9b7-88ae0a340e.zip index d19d36327..396012680 100644 Binary files a/.yarn/cache/@standardnotes-api-npm-1.12.1-6b8bfe4ccf-8623fc82de.zip and b/.yarn/cache/@standardnotes-api-npm-1.15.0-12a67ff9b7-88ae0a340e.zip differ diff --git a/.yarn/cache/@standardnotes-common-npm-1.39.0-1e36e2ef74-92cfad04a6.zip b/.yarn/cache/@standardnotes-common-npm-1.39.0-1e36e2ef74-92cfad04a6.zip new file mode 100644 index 000000000..85ed6ebe6 Binary files /dev/null and b/.yarn/cache/@standardnotes-common-npm-1.39.0-1e36e2ef74-92cfad04a6.zip differ diff --git a/.yarn/cache/@standardnotes-encryption-npm-1.16.2-9e53125abe-50efc1b201.zip b/.yarn/cache/@standardnotes-encryption-npm-1.17.0-587d631df2-587516dfed.zip similarity index 92% rename from .yarn/cache/@standardnotes-encryption-npm-1.16.2-9e53125abe-50efc1b201.zip rename to .yarn/cache/@standardnotes-encryption-npm-1.17.0-587d631df2-587516dfed.zip index 415ba6e7e..2edd06095 100644 Binary files a/.yarn/cache/@standardnotes-encryption-npm-1.16.2-9e53125abe-50efc1b201.zip and b/.yarn/cache/@standardnotes-encryption-npm-1.17.0-587d631df2-587516dfed.zip differ diff --git a/.yarn/cache/@standardnotes-features-npm-1.52.4-05c59084e4-aea7b48627.zip b/.yarn/cache/@standardnotes-features-npm-1.53.0-8ea4a2d559-a856e815a3.zip similarity index 90% rename from .yarn/cache/@standardnotes-features-npm-1.52.4-05c59084e4-aea7b48627.zip rename to .yarn/cache/@standardnotes-features-npm-1.53.0-8ea4a2d559-a856e815a3.zip index 1ddfdc2e6..a7065aea4 100644 Binary files a/.yarn/cache/@standardnotes-features-npm-1.52.4-05c59084e4-aea7b48627.zip and b/.yarn/cache/@standardnotes-features-npm-1.53.0-8ea4a2d559-a856e815a3.zip differ diff --git a/.yarn/cache/@standardnotes-models-npm-1.24.2-8c2c157efa-17b3cfba39.zip b/.yarn/cache/@standardnotes-models-npm-1.26.0-dade8919ab-f595a3de88.zip similarity index 87% rename from .yarn/cache/@standardnotes-models-npm-1.24.2-8c2c157efa-17b3cfba39.zip rename to .yarn/cache/@standardnotes-models-npm-1.26.0-dade8919ab-f595a3de88.zip index 371848783..5e9154a59 100644 Binary files a/.yarn/cache/@standardnotes-models-npm-1.24.2-8c2c157efa-17b3cfba39.zip and b/.yarn/cache/@standardnotes-models-npm-1.26.0-dade8919ab-f595a3de88.zip differ diff --git a/.yarn/cache/@standardnotes-responses-npm-1.10.6-f636794f47-0583e2cb77.zip b/.yarn/cache/@standardnotes-responses-npm-1.11.0-d066ddbbb6-46d6a47980.zip similarity index 98% rename from .yarn/cache/@standardnotes-responses-npm-1.10.6-f636794f47-0583e2cb77.zip rename to .yarn/cache/@standardnotes-responses-npm-1.11.0-d066ddbbb6-46d6a47980.zip index 18487c211..2ac32d56f 100644 Binary files a/.yarn/cache/@standardnotes-responses-npm-1.10.6-f636794f47-0583e2cb77.zip and b/.yarn/cache/@standardnotes-responses-npm-1.11.0-d066ddbbb6-46d6a47980.zip differ diff --git a/.yarn/cache/@standardnotes-utils-npm-1.9.1-e48d87ffc7-f775bb3744.zip b/.yarn/cache/@standardnotes-utils-npm-1.10.0-0dc2ade40b-c02d54ca8a.zip similarity index 94% rename from .yarn/cache/@standardnotes-utils-npm-1.9.1-e48d87ffc7-f775bb3744.zip rename to .yarn/cache/@standardnotes-utils-npm-1.10.0-0dc2ade40b-c02d54ca8a.zip index af6ae41f5..9177dddde 100644 Binary files a/.yarn/cache/@standardnotes-utils-npm-1.9.1-e48d87ffc7-f775bb3744.zip and b/.yarn/cache/@standardnotes-utils-npm-1.10.0-0dc2ade40b-c02d54ca8a.zip differ diff --git a/packages/api-gateway/bin/server.ts b/packages/api-gateway/bin/server.ts index dc87004cf..e28841560 100644 --- a/packages/api-gateway/bin/server.ts +++ b/packages/api-gateway/bin/server.ts @@ -20,6 +20,7 @@ import '../src/Controller/v1/OfflineController' import '../src/Controller/v1/FilesController' import '../src/Controller/v1/SubscriptionInvitesController' import '../src/Controller/v1/WorkspacesController' +import '../src/Controller/v1/InvitesController' import '../src/Controller/v2/PaymentsControllerV2' import '../src/Controller/v2/ActionsControllerV2' diff --git a/packages/api-gateway/src/Controller/v1/InvitesController.ts b/packages/api-gateway/src/Controller/v1/InvitesController.ts new file mode 100644 index 000000000..69cec3fb6 --- /dev/null +++ b/packages/api-gateway/src/Controller/v1/InvitesController.ts @@ -0,0 +1,23 @@ +import { inject } from 'inversify' +import { Request, Response } from 'express' +import { controller, BaseHttpController, httpPost } from 'inversify-express-utils' + +import TYPES from '../../Bootstrap/Types' +import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface' + +@controller('/v1/invites', TYPES.AuthMiddleware) +export class InvitesController extends BaseHttpController { + constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) { + super() + } + + @httpPost('/:inviteUuid/accept') + async accept(request: Request, response: Response): Promise { + await this.httpService.callWorkspaceServer( + request, + response, + `invites/${request.params.inviteUuid}/accept`, + request.body, + ) + } +} diff --git a/packages/api-gateway/src/Controller/v1/WorkspacesController.ts b/packages/api-gateway/src/Controller/v1/WorkspacesController.ts index 0bb93908e..c08ae8c7f 100644 --- a/packages/api-gateway/src/Controller/v1/WorkspacesController.ts +++ b/packages/api-gateway/src/Controller/v1/WorkspacesController.ts @@ -1,6 +1,6 @@ import { inject } from 'inversify' import { Request, Response } from 'express' -import { controller, BaseHttpController, httpPost } from 'inversify-express-utils' +import { controller, BaseHttpController, httpPost, httpGet } from 'inversify-express-utils' import TYPES from '../../Bootstrap/Types' import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface' @@ -16,6 +16,21 @@ export class WorkspacesController extends BaseHttpController { await this.httpService.callWorkspaceServer(request, response, 'workspaces', request.body) } + @httpGet('/:workspaceUuid/users') + async listWorkspaceUsers(request: Request, response: Response): Promise { + await this.httpService.callWorkspaceServer( + request, + response, + `workspaces/${request.params.workspaceUuid}/users`, + request.body, + ) + } + + @httpGet('/') + async listWorkspaces(request: Request, response: Response): Promise { + await this.httpService.callWorkspaceServer(request, response, 'workspaces', request.body) + } + @httpPost('/:workspaceUuid/invites') async invite(request: Request, response: Response): Promise { await this.httpService.callWorkspaceServer( diff --git a/packages/auth/package.json b/packages/auth/package.json index 16cd9657d..65f006f33 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -34,7 +34,7 @@ "@newrelic/winston-enricher": "^4.0.0", "@sentry/node": "^7.3.0", "@standardnotes/analytics": "workspace:*", - "@standardnotes/api": "^1.12.1", + "@standardnotes/api": "^1.15.0", "@standardnotes/common": "workspace:*", "@standardnotes/domain-events": "workspace:*", "@standardnotes/domain-events-infra": "workspace:*", diff --git a/packages/workspace/bin/server.ts b/packages/workspace/bin/server.ts index 91b421471..e1b25b03a 100644 --- a/packages/workspace/bin/server.ts +++ b/packages/workspace/bin/server.ts @@ -5,6 +5,7 @@ import 'newrelic' import * as Sentry from '@sentry/node' import '../src/Infra/InversifyExpressUtils/InversifyExpressHealthCheckController' +import '../src/Infra/InversifyExpressUtils/InversifyExpressInvitesController' import '../src/Infra/InversifyExpressUtils/InversifyExpressWorkspacesController' import * as cors from 'cors' diff --git a/packages/workspace/package.json b/packages/workspace/package.json index 9f46822e1..a3abe30ee 100644 --- a/packages/workspace/package.json +++ b/packages/workspace/package.json @@ -25,10 +25,11 @@ "dependencies": { "@newrelic/winston-enricher": "^4.0.0", "@sentry/node": "^7.3.0", - "@standardnotes/api": "^1.12.1", + "@standardnotes/api": "^1.15.0", "@standardnotes/common": "workspace:*", "@standardnotes/domain-events": "workspace:^", "@standardnotes/domain-events-infra": "workspace:^", + "@standardnotes/models": "^1.26.0", "@standardnotes/security": "workspace:*", "@standardnotes/time": "workspace:^", "aws-sdk": "^2.1159.0", diff --git a/packages/workspace/src/Bootstrap/Container.ts b/packages/workspace/src/Bootstrap/Container.ts index 502d09628..a8b0d2be6 100644 --- a/packages/workspace/src/Bootstrap/Container.ts +++ b/packages/workspace/src/Bootstrap/Container.ts @@ -38,6 +38,14 @@ import { WorkspaceInvite } from '../Domain/Invite/WorkspaceInvite' import { InviteToWorkspace } from '../Domain/UseCase/InviteToWorkspace/InviteToWorkspace' import { DomainEventFactory } from '../Domain/Event/DomainEventFactory' import { DomainEventFactoryInterface } from '../Domain/Event/DomainEventFactoryInterface' +import { WorkspaceProjection } from '../Domain/Projection/WorkspaceProjection' +import { WorkspaceProjector } from '../Domain/Projection/WorkspaceProjector' +import { ProjectorInterface } from '../Domain/Projection/ProjectorInterface' +import { WorkspaceUserProjection } from '../Domain/Projection/WorkspaceUserProjection' +import { WorkspaceUserProjector } from '../Domain/Projection/WorkspaceUserProjector' +import { AcceptInvitation } from '../Domain/UseCase/AcceptInvitation/AcceptInvitation' +import { ListWorkspaces } from '../Domain/UseCase/ListWorkspaces/ListWorkspaces' +import { ListWorkspaceUsers } from '../Domain/UseCase/ListWorkspaceUsers/ListWorkspaceUsers' // eslint-disable-next-line @typescript-eslint/no-var-requires const newrelicFormatter = require('@newrelic/winston-enricher') @@ -131,8 +139,16 @@ export class ContainerConfigLoader { // use cases container.bind(TYPES.CreateWorkspace).to(CreateWorkspace) container.bind(TYPES.InviteToWorkspace).to(InviteToWorkspace) + container.bind(TYPES.AcceptInvitation).to(AcceptInvitation) + container.bind(TYPES.ListWorkspaces).to(ListWorkspaces) + container.bind(TYPES.ListWorkspaceUsers).to(ListWorkspaceUsers) // Handlers container.bind(TYPES.UserRegisteredEventHandler).to(UserRegisteredEventHandler) + // Projection + container.bind>(TYPES.WorkspaceProjector).to(WorkspaceProjector) + container + .bind>(TYPES.WorkspaceUserProjector) + .to(WorkspaceUserProjector) // Services container.bind(TYPES.DomainEventFactory).to(DomainEventFactory) container.bind(TYPES.Timer).toConstantValue(new Timer()) diff --git a/packages/workspace/src/Bootstrap/Types.ts b/packages/workspace/src/Bootstrap/Types.ts index ba7c9afa0..5f9c0f9e2 100644 --- a/packages/workspace/src/Bootstrap/Types.ts +++ b/packages/workspace/src/Bootstrap/Types.ts @@ -28,8 +28,14 @@ const TYPES = { // use cases CreateWorkspace: Symbol.for('CreateWorkspace'), InviteToWorkspace: Symbol.for('InviteToWorkspace'), + AcceptInvitation: Symbol.for('AcceptInvitation'), + ListWorkspaces: Symbol.for('ListWorkspaces'), + ListWorkspaceUsers: Symbol.for('ListWorkspaceUsers'), // Handlers UserRegisteredEventHandler: Symbol.for('UserRegisteredEventHandler'), + // Projection + WorkspaceProjector: Symbol.for('WorkspaceProjector'), + WorkspaceUserProjector: Symbol.for('WorkspaceUserProjector'), // Services Timer: Symbol.for('Timer'), CrossServiceTokenDecoder: Symbol.for('CrossServiceTokenDecoder'), diff --git a/packages/workspace/src/Controller/WorkspacesController.spec.ts b/packages/workspace/src/Controller/WorkspacesController.spec.ts index b8ca9d3ce..f42eadbe4 100644 --- a/packages/workspace/src/Controller/WorkspacesController.spec.ts +++ b/packages/workspace/src/Controller/WorkspacesController.spec.ts @@ -1,16 +1,42 @@ import { WorkspaceAccessLevel, WorkspaceType } from '@standardnotes/common' import 'reflect-metadata' +import { ProjectorInterface } from '../Domain/Projection/ProjectorInterface' +import { WorkspaceProjection } from '../Domain/Projection/WorkspaceProjection' +import { WorkspaceUserProjection } from '../Domain/Projection/WorkspaceUserProjection' +import { AcceptInvitation } from '../Domain/UseCase/AcceptInvitation/AcceptInvitation' import { CreateWorkspace } from '../Domain/UseCase/CreateWorkspace/CreateWorkspace' import { InviteToWorkspace } from '../Domain/UseCase/InviteToWorkspace/InviteToWorkspace' +import { ListWorkspaces } from '../Domain/UseCase/ListWorkspaces/ListWorkspaces' +import { ListWorkspaceUsers } from '../Domain/UseCase/ListWorkspaceUsers/ListWorkspaceUsers' +import { Workspace } from '../Domain/Workspace/Workspace' +import { WorkspaceUser } from '../Domain/Workspace/WorkspaceUser' import { WorkspacesController } from './WorkspacesController' describe('WorkspacesController', () => { let doCreateWorkspace: CreateWorkspace let doInviteToWorkspace: InviteToWorkspace + let doAcceptInvitation: AcceptInvitation + let doListWorkspaces: ListWorkspaces + let doListWorkspaceUsers: ListWorkspaceUsers + let workspacesProject: ProjectorInterface + let workspaceUsersProjector: ProjectorInterface + let workspace1: Workspace + let workspace2: Workspace + let workspaceUser1: WorkspaceUser + let workspaceUser2: WorkspaceUser - const createController = () => new WorkspacesController(doCreateWorkspace, doInviteToWorkspace) + const createController = () => + new WorkspacesController( + doCreateWorkspace, + doInviteToWorkspace, + doListWorkspaces, + doListWorkspaceUsers, + doAcceptInvitation, + workspacesProject, + workspaceUsersProjector, + ) beforeEach(() => { doCreateWorkspace = {} as jest.Mocked @@ -18,6 +44,23 @@ describe('WorkspacesController', () => { doInviteToWorkspace = {} as jest.Mocked doInviteToWorkspace.execute = jest.fn().mockReturnValue({ invite: { uuid: 'i-1-2-3' } }) + + doListWorkspaces = {} as jest.Mocked + doListWorkspaces.execute = jest + .fn() + .mockReturnValue({ ownedWorkspaces: [workspace1], joinedWorkspaces: [workspace2] }) + + doListWorkspaceUsers = {} as jest.Mocked + doListWorkspaceUsers.execute = jest.fn().mockReturnValue({ workspaceUsers: [workspaceUser1, workspaceUser2] }) + + doAcceptInvitation = {} as jest.Mocked + doAcceptInvitation.execute = jest.fn().mockReturnValue({ success: true }) + + workspacesProject = {} as jest.Mocked> + workspacesProject.project = jest.fn().mockReturnValue({ foo: 'bar' }) + + workspaceUsersProjector = {} as jest.Mocked> + workspaceUsersProjector.project = jest.fn().mockReturnValue({ bar: 'buzz' }) }) it('should create a workspace', async () => { @@ -52,4 +95,67 @@ describe('WorkspacesController', () => { status: 200, }) }) + + it('should accept an invite', async () => { + const result = await createController().acceptInvite({ + userUuid: '1-2-3', + encryptedPrivateKey: 'foo', + inviteUuid: 'i-1-2-3', + publicKey: 'bar', + }) + + expect(result).toEqual({ + data: { + success: true, + }, + status: 200, + }) + }) + + it('should not accept an invite if it fails', async () => { + doAcceptInvitation.execute = jest.fn().mockReturnValue({ success: false }) + const result = await createController().acceptInvite({ + userUuid: '1-2-3', + encryptedPrivateKey: 'foo', + inviteUuid: 'i-1-2-3', + publicKey: 'bar', + }) + + expect(result).toEqual({ + data: { + error: { + message: 'Could not accept invite', + }, + }, + status: 400, + }) + }) + + it('should list workspaces', async () => { + const result = await createController().listWorkspaces({ + userUuid: '1-2-3', + }) + + expect(result).toEqual({ + data: { + ownedWorkspaces: [{ foo: 'bar' }], + joinedWorkspaces: [{ foo: 'bar' }], + }, + status: 200, + }) + }) + + it('should list workspace users', async () => { + const result = await createController().listWorkspaceUsers({ + userUuid: '1-2-3', + workspaceUuid: 'w-1-2-3', + }) + + expect(result).toEqual({ + data: { + users: [{ bar: 'buzz' }, { bar: 'buzz' }], + }, + status: 200, + }) + }) }) diff --git a/packages/workspace/src/Controller/WorkspacesController.ts b/packages/workspace/src/Controller/WorkspacesController.ts index ddd25edaf..62d700c09 100644 --- a/packages/workspace/src/Controller/WorkspacesController.ts +++ b/packages/workspace/src/Controller/WorkspacesController.ts @@ -6,18 +6,38 @@ import { WorkspaceInvitationRequestParams, WorkspaceInvitationResponse, WorkspaceServerInterface, + WorkspaceListRequestParams, + WorkspaceListResponse, + WorkspaceInvitationAcceptingRequestParams, + WorkspaceInvitationAcceptingResponse, + WorkspaceUserListRequestParams, } from '@standardnotes/api' import { Uuid, WorkspaceAccessLevel, WorkspaceType } from '@standardnotes/common' import TYPES from '../Bootstrap/Types' import { CreateWorkspace } from '../Domain/UseCase/CreateWorkspace/CreateWorkspace' import { InviteToWorkspace } from '../Domain/UseCase/InviteToWorkspace/InviteToWorkspace' +import { ProjectorInterface } from '../Domain/Projection/ProjectorInterface' +import { WorkspaceProjection } from '../Domain/Projection/WorkspaceProjection' +import { Workspace } from '../Domain/Workspace/Workspace' +import { ListWorkspaces } from '../Domain/UseCase/ListWorkspaces/ListWorkspaces' +import { WorkspaceUserListResponse } from '@standardnotes/api/dist/Domain/Response/Workspace/WorkspaceUserListResponse' +import { AcceptInvitation } from '../Domain/UseCase/AcceptInvitation/AcceptInvitation' +import { WorkspaceUser } from '../Domain/Workspace/WorkspaceUser' +import { WorkspaceUserProjection } from '../Domain/Projection/WorkspaceUserProjection' +import { ListWorkspaceUsers } from '../Domain/UseCase/ListWorkspaceUsers/ListWorkspaceUsers' @injectable() export class WorkspacesController implements WorkspaceServerInterface { constructor( @inject(TYPES.CreateWorkspace) private doCreateWorkspace: CreateWorkspace, @inject(TYPES.InviteToWorkspace) private doInviteToWorkspace: InviteToWorkspace, + @inject(TYPES.ListWorkspaces) private doListWorkspaces: ListWorkspaces, + @inject(TYPES.ListWorkspaceUsers) private doListWorkspaceUsers: ListWorkspaceUsers, + @inject(TYPES.AcceptInvitation) private doAcceptInvite: AcceptInvitation, + @inject(TYPES.WorkspaceProjector) private workspaceProjector: ProjectorInterface, + @inject(TYPES.WorkspaceUserProjector) + private workspaceUserProjector: ProjectorInterface, ) {} async inviteToWorkspace(params: WorkspaceInvitationRequestParams): Promise { @@ -49,4 +69,69 @@ export class WorkspacesController implements WorkspaceServerInterface { data: { uuid: workspace.uuid }, } } + + async listWorkspaces(params: WorkspaceListRequestParams): Promise { + const { ownedWorkspaces, joinedWorkspaces } = await this.doListWorkspaces.execute({ + userUuid: params.userUuid as Uuid, + }) + + const ownedWorkspacesProjections = [] + for (const ownedWorkspace of ownedWorkspaces) { + ownedWorkspacesProjections.push(await this.workspaceProjector.project(ownedWorkspace)) + } + + const joinedWorkspacesProjections = [] + for (const joinedWorkspace of joinedWorkspaces) { + joinedWorkspacesProjections.push(await this.workspaceProjector.project(joinedWorkspace)) + } + + return { + status: HttpStatusCode.Success, + data: { ownedWorkspaces: ownedWorkspacesProjections, joinedWorkspaces: joinedWorkspacesProjections }, + } + } + + async listWorkspaceUsers(params: WorkspaceUserListRequestParams): Promise { + const { workspaceUsers } = await this.doListWorkspaceUsers.execute({ + userUuid: params.userUuid as Uuid, + workspaceUuid: params.workspaceUuid, + }) + + const workspaceUserProjections = [] + for (const workspaceUser of workspaceUsers) { + workspaceUserProjections.push(await this.workspaceUserProjector.project(workspaceUser)) + } + + return { + status: HttpStatusCode.Success, + data: { users: workspaceUserProjections }, + } + } + + async acceptInvite(params: WorkspaceInvitationAcceptingRequestParams): Promise { + const result = await this.doAcceptInvite.execute({ + acceptingUserUuid: params.userUuid, + encryptedPrivateKey: params.encryptedPrivateKey, + invitationUuid: params.inviteUuid, + publicKey: params.publicKey, + }) + + if (!result.success) { + return { + status: HttpStatusCode.BadRequest, + data: { + error: { + message: 'Could not accept invite', + }, + }, + } + } + + return { + status: HttpStatusCode.Success, + data: { + success: true, + }, + } + } } diff --git a/packages/workspace/src/Domain/Projection/ProjectorInterface.ts b/packages/workspace/src/Domain/Projection/ProjectorInterface.ts new file mode 100644 index 000000000..a987e78be --- /dev/null +++ b/packages/workspace/src/Domain/Projection/ProjectorInterface.ts @@ -0,0 +1,3 @@ +export interface ProjectorInterface { + project(object: T): Promise +} diff --git a/packages/workspace/src/Domain/Projection/WorkspaceProjection.ts b/packages/workspace/src/Domain/Projection/WorkspaceProjection.ts new file mode 100644 index 000000000..4ddd29fd5 --- /dev/null +++ b/packages/workspace/src/Domain/Projection/WorkspaceProjection.ts @@ -0,0 +1,3 @@ +import { Workspace } from '@standardnotes/models' + +export type WorkspaceProjection = Workspace diff --git a/packages/workspace/src/Domain/Projection/WorkspaceProjector.spec.ts b/packages/workspace/src/Domain/Projection/WorkspaceProjector.spec.ts new file mode 100644 index 000000000..103c95406 --- /dev/null +++ b/packages/workspace/src/Domain/Projection/WorkspaceProjector.spec.ts @@ -0,0 +1,30 @@ +import 'reflect-metadata' + +import { WorkspaceType } from '@standardnotes/common' +import { Workspace } from '../Workspace/Workspace' + +import { WorkspaceProjector } from './WorkspaceProjector' + +describe('WorkspaceProjector', () => { + const createProjector = () => new WorkspaceProjector() + + it('should project a workspace', async () => { + expect( + await createProjector().project({ + uuid: 'w-1-2-3', + type: WorkspaceType.Private, + name: 'test', + keyRotationIndex: 0, + createdAt: 1, + updatedAt: 2, + } as jest.Mocked), + ).toEqual({ + uuid: 'w-1-2-3', + type: 'private', + name: 'test', + keyRotationIndex: 0, + createdAt: 1, + updatedAt: 2, + }) + }) +}) diff --git a/packages/workspace/src/Domain/Projection/WorkspaceProjector.ts b/packages/workspace/src/Domain/Projection/WorkspaceProjector.ts new file mode 100644 index 000000000..2d798b360 --- /dev/null +++ b/packages/workspace/src/Domain/Projection/WorkspaceProjector.ts @@ -0,0 +1,19 @@ +import { injectable } from 'inversify' +import { ProjectorInterface } from './ProjectorInterface' + +import { WorkspaceProjection } from './WorkspaceProjection' +import { Workspace } from '../Workspace/Workspace' + +@injectable() +export class WorkspaceProjector implements ProjectorInterface { + async project(workspace: Workspace): Promise { + return { + uuid: workspace.uuid, + type: workspace.type, + name: workspace.name, + keyRotationIndex: workspace.keyRotationIndex, + createdAt: workspace.createdAt, + updatedAt: workspace.updatedAt, + } + } +} diff --git a/packages/workspace/src/Domain/Projection/WorkspaceUserProjection.ts b/packages/workspace/src/Domain/Projection/WorkspaceUserProjection.ts new file mode 100644 index 000000000..966f22d84 --- /dev/null +++ b/packages/workspace/src/Domain/Projection/WorkspaceUserProjection.ts @@ -0,0 +1,3 @@ +import { WorkspaceUser } from '@standardnotes/models' + +export type WorkspaceUserProjection = WorkspaceUser diff --git a/packages/workspace/src/Domain/Projection/WorkspaceUserProjector.spec.ts b/packages/workspace/src/Domain/Projection/WorkspaceUserProjector.spec.ts new file mode 100644 index 000000000..35a54b4c6 --- /dev/null +++ b/packages/workspace/src/Domain/Projection/WorkspaceUserProjector.spec.ts @@ -0,0 +1,42 @@ +import 'reflect-metadata' + +import { WorkspaceAccessLevel, WorkspaceUserStatus } from '@standardnotes/common' +import { WorkspaceUser } from '../Workspace/WorkspaceUser' + +import { WorkspaceUserProjector } from './WorkspaceUserProjector' + +describe('WorkspaceUserProjector', () => { + const createProjector = () => new WorkspaceUserProjector() + + it('should project a workspace user', async () => { + expect( + await createProjector().project({ + uuid: '1-2-3', + accessLevel: WorkspaceAccessLevel.Owner, + userUuid: 'u-1-2-3', + userDisplayName: 'foobar', + workspaceUuid: 'w-1-2-3', + encryptedWorkspaceKey: 'foo', + publicKey: 'bar', + encryptedPrivateKey: 'buzz', + status: WorkspaceUserStatus.PendingKeyshare, + keyRotationIndex: 0, + createdAt: 1, + updatedAt: 2, + } as jest.Mocked), + ).toEqual({ + uuid: '1-2-3', + accessLevel: 'owner', + userUuid: 'u-1-2-3', + userDisplayName: 'foobar', + workspaceUuid: 'w-1-2-3', + encryptedWorkspaceKey: 'foo', + publicKey: 'bar', + encryptedPrivateKey: 'buzz', + status: 'pending-keyshare', + keyRotationIndex: 0, + createdAt: 1, + updatedAt: 2, + }) + }) +}) diff --git a/packages/workspace/src/Domain/Projection/WorkspaceUserProjector.ts b/packages/workspace/src/Domain/Projection/WorkspaceUserProjector.ts new file mode 100644 index 000000000..613b88160 --- /dev/null +++ b/packages/workspace/src/Domain/Projection/WorkspaceUserProjector.ts @@ -0,0 +1,25 @@ +import { injectable } from 'inversify' +import { ProjectorInterface } from './ProjectorInterface' + +import { WorkspaceUserProjection } from './WorkspaceUserProjection' +import { WorkspaceUser } from '../Workspace/WorkspaceUser' + +@injectable() +export class WorkspaceUserProjector implements ProjectorInterface { + async project(workspaceUser: WorkspaceUser): Promise { + return { + uuid: workspaceUser.uuid, + accessLevel: workspaceUser.accessLevel, + userUuid: workspaceUser.userUuid, + userDisplayName: workspaceUser.userDisplayName, + workspaceUuid: workspaceUser.workspaceUuid, + encryptedWorkspaceKey: workspaceUser.encryptedWorkspaceKey, + publicKey: workspaceUser.publicKey, + encryptedPrivateKey: workspaceUser.encryptedPrivateKey, + status: workspaceUser.status, + keyRotationIndex: workspaceUser.keyRotationIndex, + createdAt: workspaceUser.createdAt, + updatedAt: workspaceUser.updatedAt, + } + } +} diff --git a/packages/workspace/src/Domain/UseCase/ListWorkspaceUsers/ListWorkspaceUsers.spec.ts b/packages/workspace/src/Domain/UseCase/ListWorkspaceUsers/ListWorkspaceUsers.spec.ts new file mode 100644 index 000000000..dbc73f058 --- /dev/null +++ b/packages/workspace/src/Domain/UseCase/ListWorkspaceUsers/ListWorkspaceUsers.spec.ts @@ -0,0 +1,84 @@ +import { WorkspaceAccessLevel } from '@standardnotes/common' +import 'reflect-metadata' +import { Workspace } from '../../Workspace/Workspace' +import { WorkspaceRepositoryInterface } from '../../Workspace/WorkspaceRepositoryInterface' +import { WorkspaceUser } from '../../Workspace/WorkspaceUser' +import { WorkspaceUserRepositoryInterface } from '../../Workspace/WorkspaceUserRepositoryInterface' + +import { ListWorkspaceUsers } from './ListWorkspaceUsers' + +describe('ListWorkspaceUsers', () => { + let workspaceRepository: WorkspaceRepositoryInterface + let workspaceUserRepository: WorkspaceUserRepositoryInterface + let workspace: Workspace + let workspaceUser1: WorkspaceUser + let workspaceUser2: WorkspaceUser + + const createUseCase = () => new ListWorkspaceUsers(workspaceRepository, workspaceUserRepository) + + beforeEach(() => { + workspace = { uuid: 'j-1-2-3' } as jest.Mocked + + workspaceUser1 = { userUuid: 'u-1-2-3', accessLevel: WorkspaceAccessLevel.Owner } as jest.Mocked + workspaceUser2 = { + userUuid: 'u-2-3-4', + accessLevel: WorkspaceAccessLevel.WriteAndRead, + } as jest.Mocked + + workspaceRepository = {} as jest.Mocked + workspaceRepository.findOneByUuid = jest.fn().mockReturnValue(workspace) + + workspaceUserRepository = {} as jest.Mocked + workspaceUserRepository.findByWorkspaceUuid = jest.fn().mockReturnValue([workspaceUser1, workspaceUser2]) + }) + + it('should list users in a workspace where the user is owner or admin', async () => { + const result = await createUseCase().execute({ + userUuid: 'u-1-2-3', + workspaceUuid: 'j-1-2-3', + }) + + expect(result).toEqual({ + workspaceUsers: [workspaceUser1, workspaceUser2], + userIsOwnerOrAdmin: true, + }) + }) + + it('should list users in a workspace where the user is not the owner or admin with indiciation', async () => { + const result = await createUseCase().execute({ + userUuid: 'u-2-3-4', + workspaceUuid: 'j-1-2-3', + }) + + expect(result).toEqual({ + workspaceUsers: [workspaceUser1, workspaceUser2], + userIsOwnerOrAdmin: false, + }) + }) + + it('should not list users in a workspace where the user does not belong', async () => { + const result = await createUseCase().execute({ + userUuid: 'z-1-2-3', + workspaceUuid: 'j-1-2-3', + }) + + expect(result).toEqual({ + workspaceUsers: [], + userIsOwnerOrAdmin: false, + }) + }) + + it('should not list users in a workspace that does not exist', async () => { + workspaceRepository.findOneByUuid = jest.fn().mockReturnValue(null) + + const result = await createUseCase().execute({ + userUuid: 'u-1-2-3', + workspaceUuid: 'j-1-2-3', + }) + + expect(result).toEqual({ + workspaceUsers: [], + userIsOwnerOrAdmin: false, + }) + }) +}) diff --git a/packages/workspace/src/Domain/UseCase/ListWorkspaceUsers/ListWorkspaceUsers.ts b/packages/workspace/src/Domain/UseCase/ListWorkspaceUsers/ListWorkspaceUsers.ts new file mode 100644 index 000000000..f364db9ab --- /dev/null +++ b/packages/workspace/src/Domain/UseCase/ListWorkspaceUsers/ListWorkspaceUsers.ts @@ -0,0 +1,50 @@ +import { WorkspaceAccessLevel } from '@standardnotes/common' +import { inject, injectable } from 'inversify' +import TYPES from '../../../Bootstrap/Types' +import { WorkspaceRepositoryInterface } from '../../Workspace/WorkspaceRepositoryInterface' +import { WorkspaceUserRepositoryInterface } from '../../Workspace/WorkspaceUserRepositoryInterface' +import { UseCaseInterface } from '../UseCaseInterface' +import { ListWorkspaceUsersDTO } from './ListWorkspaceUsersDTO' +import { ListWorkspaceUsersResponse } from './ListWorkspaceUsersResponse' + +@injectable() +export class ListWorkspaceUsers implements UseCaseInterface { + constructor( + @inject(TYPES.WorkspaceRepository) private workspaceRepository: WorkspaceRepositoryInterface, + @inject(TYPES.WorkspaceUserRepository) private workspaceUserRepository: WorkspaceUserRepositoryInterface, + ) {} + + async execute(dto: ListWorkspaceUsersDTO): Promise { + const workspace = await this.workspaceRepository.findOneByUuid(dto.workspaceUuid) + if (workspace === null) { + return { + workspaceUsers: [], + userIsOwnerOrAdmin: false, + } + } + + const workspaceUsers = await this.workspaceUserRepository.findByWorkspaceUuid(dto.workspaceUuid) + let userIsOwnerOrAdmin = false + let userIsInWorkspace = false + for (const workspaceUser of workspaceUsers) { + if (workspaceUser.userUuid === dto.userUuid) { + userIsInWorkspace = true + if ([WorkspaceAccessLevel.Admin, WorkspaceAccessLevel.Owner].includes(workspaceUser.accessLevel)) { + userIsOwnerOrAdmin = true + } + } + } + + if (!userIsInWorkspace) { + return { + workspaceUsers: [], + userIsOwnerOrAdmin: false, + } + } + + return { + workspaceUsers, + userIsOwnerOrAdmin, + } + } +} diff --git a/packages/workspace/src/Domain/UseCase/ListWorkspaceUsers/ListWorkspaceUsersDTO.ts b/packages/workspace/src/Domain/UseCase/ListWorkspaceUsers/ListWorkspaceUsersDTO.ts new file mode 100644 index 000000000..bff27c6d5 --- /dev/null +++ b/packages/workspace/src/Domain/UseCase/ListWorkspaceUsers/ListWorkspaceUsersDTO.ts @@ -0,0 +1,6 @@ +import { Uuid } from '@standardnotes/common' + +export type ListWorkspaceUsersDTO = { + workspaceUuid: Uuid + userUuid: Uuid +} diff --git a/packages/workspace/src/Domain/UseCase/ListWorkspaceUsers/ListWorkspaceUsersResponse.ts b/packages/workspace/src/Domain/UseCase/ListWorkspaceUsers/ListWorkspaceUsersResponse.ts new file mode 100644 index 000000000..2e51113f4 --- /dev/null +++ b/packages/workspace/src/Domain/UseCase/ListWorkspaceUsers/ListWorkspaceUsersResponse.ts @@ -0,0 +1,6 @@ +import { WorkspaceUser } from '../../Workspace/WorkspaceUser' + +export type ListWorkspaceUsersResponse = { + workspaceUsers: WorkspaceUser[] + userIsOwnerOrAdmin: boolean +} diff --git a/packages/workspace/src/Domain/UseCase/ListWorkspaces/ListWorkspaces.spec.ts b/packages/workspace/src/Domain/UseCase/ListWorkspaces/ListWorkspaces.spec.ts new file mode 100644 index 000000000..c0a88d98e --- /dev/null +++ b/packages/workspace/src/Domain/UseCase/ListWorkspaces/ListWorkspaces.spec.ts @@ -0,0 +1,47 @@ +import { WorkspaceAccessLevel } from '@standardnotes/common' +import 'reflect-metadata' +import { Workspace } from '../../Workspace/Workspace' +import { WorkspaceRepositoryInterface } from '../../Workspace/WorkspaceRepositoryInterface' +import { WorkspaceUser } from '../../Workspace/WorkspaceUser' +import { WorkspaceUserRepositoryInterface } from '../../Workspace/WorkspaceUserRepositoryInterface' + +import { ListWorkspaces } from './ListWorkspaces' + +describe('ListWorkspaces', () => { + let workspaceRepository: WorkspaceRepositoryInterface + let workspaceUserRepository: WorkspaceUserRepositoryInterface + let ownedWorkspace: Workspace + let joinedWorkspace: Workspace + let workspaceUser1: WorkspaceUser + let workspaceUser2: WorkspaceUser + + const createUseCase = () => new ListWorkspaces(workspaceRepository, workspaceUserRepository) + + beforeEach(() => { + ownedWorkspace = { uuid: 'o-1-2-3' } as jest.Mocked + joinedWorkspace = { uuid: 'j-1-2-3' } as jest.Mocked + + workspaceUser1 = { accessLevel: WorkspaceAccessLevel.Owner } as jest.Mocked + workspaceUser2 = { accessLevel: WorkspaceAccessLevel.WriteAndRead } as jest.Mocked + + workspaceRepository = {} as jest.Mocked + workspaceRepository.findByUuids = jest + .fn() + .mockReturnValueOnce([ownedWorkspace]) + .mockReturnValueOnce([joinedWorkspace]) + + workspaceUserRepository = {} as jest.Mocked + workspaceUserRepository.findByUserUuid = jest.fn().mockReturnValue([workspaceUser1, workspaceUser2]) + }) + + it('should list owned and joined workspaces for a user', async () => { + const result = await createUseCase().execute({ + userUuid: 'u-1-2-3', + }) + + expect(result).toEqual({ + ownedWorkspaces: [ownedWorkspace], + joinedWorkspaces: [joinedWorkspace], + }) + }) +}) diff --git a/packages/workspace/src/Domain/UseCase/ListWorkspaces/ListWorkspaces.ts b/packages/workspace/src/Domain/UseCase/ListWorkspaces/ListWorkspaces.ts new file mode 100644 index 000000000..00c18bb97 --- /dev/null +++ b/packages/workspace/src/Domain/UseCase/ListWorkspaces/ListWorkspaces.ts @@ -0,0 +1,40 @@ +import { WorkspaceAccessLevel } from '@standardnotes/common' +import { inject, injectable } from 'inversify' + +import TYPES from '../../../Bootstrap/Types' +import { WorkspaceRepositoryInterface } from '../../Workspace/WorkspaceRepositoryInterface' +import { WorkspaceUserRepositoryInterface } from '../../Workspace/WorkspaceUserRepositoryInterface' +import { UseCaseInterface } from '../UseCaseInterface' + +import { ListWorkspacesDTO } from './ListWorkspacesDTO' +import { ListWorkspacesResponse } from './ListWorkspacesResponse' + +@injectable() +export class ListWorkspaces implements UseCaseInterface { + constructor( + @inject(TYPES.WorkspaceRepository) private workspaceRepository: WorkspaceRepositoryInterface, + @inject(TYPES.WorkspaceUserRepository) private workspaceUserRepository: WorkspaceUserRepositoryInterface, + ) {} + + async execute(dto: ListWorkspacesDTO): Promise { + const workspaceAssociations = await this.workspaceUserRepository.findByUserUuid(dto.userUuid) + + const ownedWorkspacesUuids = [] + const joinedWorkspacesUuids = [] + for (const workspaceAssociation of workspaceAssociations) { + if ([WorkspaceAccessLevel.Admin, WorkspaceAccessLevel.Owner].includes(workspaceAssociation.accessLevel)) { + ownedWorkspacesUuids.push(workspaceAssociation.uuid) + } else { + joinedWorkspacesUuids.push(workspaceAssociation.uuid) + } + } + + const ownedWorkspaces = await this.workspaceRepository.findByUuids(ownedWorkspacesUuids) + const joinedWorkspaces = await this.workspaceRepository.findByUuids(joinedWorkspacesUuids) + + return { + ownedWorkspaces, + joinedWorkspaces, + } + } +} diff --git a/packages/workspace/src/Domain/UseCase/ListWorkspaces/ListWorkspacesDTO.ts b/packages/workspace/src/Domain/UseCase/ListWorkspaces/ListWorkspacesDTO.ts new file mode 100644 index 000000000..b70037c69 --- /dev/null +++ b/packages/workspace/src/Domain/UseCase/ListWorkspaces/ListWorkspacesDTO.ts @@ -0,0 +1,5 @@ +import { Uuid } from '@standardnotes/common' + +export type ListWorkspacesDTO = { + userUuid: Uuid +} diff --git a/packages/workspace/src/Domain/UseCase/ListWorkspaces/ListWorkspacesResponse.ts b/packages/workspace/src/Domain/UseCase/ListWorkspaces/ListWorkspacesResponse.ts new file mode 100644 index 000000000..8c860d8d0 --- /dev/null +++ b/packages/workspace/src/Domain/UseCase/ListWorkspaces/ListWorkspacesResponse.ts @@ -0,0 +1,6 @@ +import { Workspace } from '../../Workspace/Workspace' + +export type ListWorkspacesResponse = { + ownedWorkspaces: Array + joinedWorkspaces: Array +} diff --git a/packages/workspace/src/Domain/Workspace/WorkspaceRepositoryInterface.ts b/packages/workspace/src/Domain/Workspace/WorkspaceRepositoryInterface.ts index 5ded5fee6..ce389c721 100644 --- a/packages/workspace/src/Domain/Workspace/WorkspaceRepositoryInterface.ts +++ b/packages/workspace/src/Domain/Workspace/WorkspaceRepositoryInterface.ts @@ -1,5 +1,8 @@ +import { Uuid } from '@standardnotes/common' import { Workspace } from './Workspace' export interface WorkspaceRepositoryInterface { save(workspace: Workspace): Promise + findByUuids(uuids: Uuid[]): Promise + findOneByUuid(uuid: Uuid): Promise } diff --git a/packages/workspace/src/Domain/Workspace/WorkspaceUserRepositoryInterface.ts b/packages/workspace/src/Domain/Workspace/WorkspaceUserRepositoryInterface.ts index bad097e18..c3594d4e0 100644 --- a/packages/workspace/src/Domain/Workspace/WorkspaceUserRepositoryInterface.ts +++ b/packages/workspace/src/Domain/Workspace/WorkspaceUserRepositoryInterface.ts @@ -1,5 +1,8 @@ +import { Uuid } from '@standardnotes/common' import { WorkspaceUser } from './WorkspaceUser' export interface WorkspaceUserRepositoryInterface { save(workspace: WorkspaceUser): Promise + findByUserUuid(userUuid: Uuid): Promise + findByWorkspaceUuid(workspaceUuid: Uuid): Promise } diff --git a/packages/workspace/src/Infra/InversifyExpressUtils/InversifyExpressInvitesController.ts b/packages/workspace/src/Infra/InversifyExpressUtils/InversifyExpressInvitesController.ts new file mode 100644 index 000000000..90f327817 --- /dev/null +++ b/packages/workspace/src/Infra/InversifyExpressUtils/InversifyExpressInvitesController.ts @@ -0,0 +1,23 @@ +import { Request, Response } from 'express' +import { inject } from 'inversify' +import { BaseHttpController, controller, httpPost, results } from 'inversify-express-utils' +import TYPES from '../../Bootstrap/Types' +import { WorkspacesController } from '../../Controller/WorkspacesController' + +@controller('/invites', TYPES.ApiGatewayAuthMiddleware) +export class InversifyExpressInvitesController extends BaseHttpController { + constructor(@inject(TYPES.WorkspacesController) private workspacesController: WorkspacesController) { + super() + } + + @httpPost('/:inviteUuid/accept') + async acceptInvite(request: Request, response: Response): Promise { + const result = await this.workspacesController.acceptInvite({ + ...request.body, + inviteUuid: request.params.inviteUuid, + userUuid: response.locals.user.uuid, + }) + + return this.json(result.data, result.status) + } +} diff --git a/packages/workspace/src/Infra/InversifyExpressUtils/InversifyExpressWorkspacesController.ts b/packages/workspace/src/Infra/InversifyExpressUtils/InversifyExpressWorkspacesController.ts index fb0cfb478..f33696d76 100644 --- a/packages/workspace/src/Infra/InversifyExpressUtils/InversifyExpressWorkspacesController.ts +++ b/packages/workspace/src/Infra/InversifyExpressUtils/InversifyExpressWorkspacesController.ts @@ -1,6 +1,6 @@ import { Request, Response } from 'express' import { inject } from 'inversify' -import { BaseHttpController, controller, httpPost, results } from 'inversify-express-utils' +import { BaseHttpController, controller, httpGet, httpPost, results } from 'inversify-express-utils' import TYPES from '../../Bootstrap/Types' import { WorkspacesController } from '../../Controller/WorkspacesController' @@ -20,6 +20,25 @@ export class InversifyExpressWorkspacesController extends BaseHttpController { return this.json(result.data, result.status) } + @httpGet('/') + async listWorkspaces(response: Response): Promise { + const result = await this.workspacesController.listWorkspaces({ + userUuid: response.locals.user.uuid, + }) + + return this.json(result.data, result.status) + } + + @httpGet('/:workspaceUuid/users') + async listWorkspaceUsers(request: Request, response: Response): Promise { + const result = await this.workspacesController.listWorkspaceUsers({ + userUuid: response.locals.user.uuid, + workspaceUuid: request.params.workspaceUuid, + }) + + return this.json(result.data, result.status) + } + @httpPost('/:workspaceUuid/invites') async inviteToWorkspace(request: Request, response: Response): Promise { if (request.params.workspaceUuid !== request.body.workspaceUuid) { diff --git a/packages/workspace/src/Infra/MySQL/MySQLWorkspaceRepository.spec.ts b/packages/workspace/src/Infra/MySQL/MySQLWorkspaceRepository.spec.ts index cd5ec4ae2..f373c643b 100644 --- a/packages/workspace/src/Infra/MySQL/MySQLWorkspaceRepository.spec.ts +++ b/packages/workspace/src/Infra/MySQL/MySQLWorkspaceRepository.spec.ts @@ -28,4 +28,22 @@ describe('MySQLWorkspaceRepository', () => { expect(ormRepository.save).toHaveBeenCalledWith(workspace) }) + + it('should find many by uuids', async () => { + queryBuilder.where = jest.fn().mockReturnThis() + queryBuilder.getMany = jest.fn().mockReturnValue([]) + + await createRepository().findByUuids(['i-1-2-3']) + + expect(queryBuilder.where).toHaveBeenCalledWith('uuid IN (:...uuids)', { uuids: ['i-1-2-3'] }) + }) + + 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/MySQLWorkspaceRepository.ts b/packages/workspace/src/Infra/MySQL/MySQLWorkspaceRepository.ts index 17547ce33..6cbceaf07 100644 --- a/packages/workspace/src/Infra/MySQL/MySQLWorkspaceRepository.ts +++ b/packages/workspace/src/Infra/MySQL/MySQLWorkspaceRepository.ts @@ -11,6 +11,14 @@ export class MySQLWorkspaceRepository implements WorkspaceRepositoryInterface { private ormRepository: Repository, ) {} + async findOneByUuid(uuid: string): Promise { + return this.ormRepository.createQueryBuilder().where('uuid = :uuid', { uuid }).getOne() + } + + async findByUuids(uuids: string[]): Promise { + return this.ormRepository.createQueryBuilder().where('uuid IN (:...uuids)', { uuids }).getMany() + } + async save(workspace: Workspace): Promise { return this.ormRepository.save(workspace) } diff --git a/packages/workspace/src/Infra/MySQL/MySQLWorkspaceUserRepository.spec.ts b/packages/workspace/src/Infra/MySQL/MySQLWorkspaceUserRepository.spec.ts index 44dbcf215..7c596b133 100644 --- a/packages/workspace/src/Infra/MySQL/MySQLWorkspaceUserRepository.spec.ts +++ b/packages/workspace/src/Infra/MySQL/MySQLWorkspaceUserRepository.spec.ts @@ -27,4 +27,22 @@ describe('MySQLWorkspaceUserRepository', () => { expect(ormRepository.save).toHaveBeenCalledWith(workspace) }) + + it('should find many by user uuid', async () => { + queryBuilder.where = jest.fn().mockReturnThis() + queryBuilder.getMany = jest.fn().mockReturnValue([]) + + await createRepository().findByUserUuid('i-1-2-3') + + expect(queryBuilder.where).toHaveBeenCalledWith('user_uuid = :userUuid', { userUuid: 'i-1-2-3' }) + }) + + it('should find many by workspace uuid', async () => { + queryBuilder.where = jest.fn().mockReturnThis() + queryBuilder.getMany = jest.fn().mockReturnValue([]) + + await createRepository().findByWorkspaceUuid('i-1-2-3') + + expect(queryBuilder.where).toHaveBeenCalledWith('workspace_uuid = :workspaceUuid', { workspaceUuid: 'i-1-2-3' }) + }) }) diff --git a/packages/workspace/src/Infra/MySQL/MySQLWorkspaceUserRepository.ts b/packages/workspace/src/Infra/MySQL/MySQLWorkspaceUserRepository.ts index 1ccfcf4f7..1a7120e4c 100644 --- a/packages/workspace/src/Infra/MySQL/MySQLWorkspaceUserRepository.ts +++ b/packages/workspace/src/Infra/MySQL/MySQLWorkspaceUserRepository.ts @@ -12,6 +12,14 @@ export class MySQLWorkspaceUserRepository implements WorkspaceUserRepositoryInte private ormRepository: Repository, ) {} + async findByWorkspaceUuid(workspaceUuid: string): Promise { + return this.ormRepository.createQueryBuilder().where('workspace_uuid = :workspaceUuid', { workspaceUuid }).getMany() + } + + async findByUserUuid(userUuid: string): Promise { + return this.ormRepository.createQueryBuilder().where('user_uuid = :userUuid', { userUuid }).getMany() + } + async save(workspaceUser: WorkspaceUser): Promise { return this.ormRepository.save(workspaceUser) } diff --git a/yarn.lock b/yarn.lock index 9f8a0971a..412f516c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1824,18 +1824,18 @@ __metadata: languageName: unknown linkType: soft -"@standardnotes/api@npm:^1.12.1": - version: 1.12.1 - resolution: "@standardnotes/api@npm:1.12.1" +"@standardnotes/api@npm:^1.15.0": + version: 1.15.0 + resolution: "@standardnotes/api@npm:1.15.0" dependencies: - "@standardnotes/common": ^1.36.1 - "@standardnotes/encryption": 1.16.2 - "@standardnotes/models": 1.24.2 - "@standardnotes/responses": 1.10.6 + "@standardnotes/common": ^1.39.0 + "@standardnotes/encryption": 1.17.0 + "@standardnotes/models": 1.26.0 + "@standardnotes/responses": 1.11.0 "@standardnotes/security": ^1.1.0 - "@standardnotes/utils": 1.9.1 + "@standardnotes/utils": 1.10.0 reflect-metadata: ^0.1.13 - checksum: 8623fc82de0cbe6793691bc50bf168d1ab2535516f71ffc10ac642abe6ab9ac2faef6cfe406350c2a1b6ea31e0ad34ad29cab804721a49500f6a1d3498cdd46e + checksum: 88ae0a340e175b8251e4960f377998a9e21ec5c7967be85c813ff2103d281faea8b13faf787ac51dab4d1737521df136748c9dbd34f0eee2f61d860085bca840 languageName: node linkType: hard @@ -1846,7 +1846,7 @@ __metadata: "@newrelic/winston-enricher": ^4.0.0 "@sentry/node": ^7.3.0 "@standardnotes/analytics": "workspace:*" - "@standardnotes/api": ^1.12.1 + "@standardnotes/api": ^1.15.0 "@standardnotes/common": "workspace:*" "@standardnotes/domain-events": "workspace:*" "@standardnotes/domain-events-infra": "workspace:*" @@ -1907,7 +1907,7 @@ __metadata: languageName: node linkType: hard -"@standardnotes/common@^1.19.1, @standardnotes/common@^1.23.1, @standardnotes/common@^1.32.0, @standardnotes/common@^1.36.1, @standardnotes/common@workspace:*, @standardnotes/common@workspace:^, @standardnotes/common@workspace:packages/common": +"@standardnotes/common@^1.19.1, @standardnotes/common@^1.23.1, @standardnotes/common@^1.32.0, @standardnotes/common@workspace:*, @standardnotes/common@workspace:^, @standardnotes/common@workspace:packages/common": version: 0.0.0-use.local resolution: "@standardnotes/common@workspace:packages/common" dependencies: @@ -1921,6 +1921,15 @@ __metadata: languageName: unknown linkType: soft +"@standardnotes/common@npm:^1.39.0": + version: 1.39.0 + resolution: "@standardnotes/common@npm:1.39.0" + dependencies: + reflect-metadata: ^0.1.13 + checksum: 92cfad04a631cdcf971516be4bf0e066e9cb6e894017102ac406cd8964282e088653ebed282d7624f371d617418edce124d12a62964723697ed7fa33b6d6e4d0 + languageName: node + linkType: hard + "@standardnotes/config@npm:2.4.3": version: 2.4.3 resolution: "@standardnotes/config@npm:2.4.3" @@ -1972,17 +1981,17 @@ __metadata: languageName: unknown linkType: soft -"@standardnotes/encryption@npm:1.16.2": - version: 1.16.2 - resolution: "@standardnotes/encryption@npm:1.16.2" +"@standardnotes/encryption@npm:1.17.0": + version: 1.17.0 + resolution: "@standardnotes/encryption@npm:1.17.0" dependencies: - "@standardnotes/common": ^1.36.1 - "@standardnotes/models": 1.24.2 - "@standardnotes/responses": 1.10.6 + "@standardnotes/common": ^1.39.0 + "@standardnotes/models": 1.26.0 + "@standardnotes/responses": 1.11.0 "@standardnotes/sncrypto-common": 1.13.0 - "@standardnotes/utils": 1.9.1 + "@standardnotes/utils": 1.10.0 reflect-metadata: ^0.1.13 - checksum: 50efc1b20105b06be2325d17440a0952e3fd47a596f99ea937446e6da895c349cb5ae449398c390829c2379c03a7cfa160e7f6b2d9bf3339ccb53fc34fa81c86 + checksum: 587516dfed87ba0bc46fef9ddcc6edb42757c140f852096d6bf6f7911418a7aa1eb0b370c5a0c56eeab3e6b13117ec055374b6e023c64e67cdf2362a584ac06e languageName: node linkType: hard @@ -2014,15 +2023,15 @@ __metadata: languageName: unknown linkType: soft -"@standardnotes/features@npm:1.52.4": - version: 1.52.4 - resolution: "@standardnotes/features@npm:1.52.4" +"@standardnotes/features@npm:1.53.0": + version: 1.53.0 + resolution: "@standardnotes/features@npm:1.53.0" dependencies: "@standardnotes/auth": ^3.19.4 - "@standardnotes/common": ^1.36.1 + "@standardnotes/common": ^1.39.0 "@standardnotes/security": ^1.2.0 reflect-metadata: ^0.1.13 - checksum: aea7b486275e9485c3d87b3db334c2b955f3ddd160054282e769aa59f026f6daffcb15edd8a3a4959a861552294df54437edb8d5b636f4e4f1c59eb74e75424b + checksum: a856e815a313d42706836b1e4c8bbaeb987ee86f2e53bf4b928ed401de483ab11ecd31a8bcb13b23cb7b6a53f72a9f481da2acf68d5e0e970af4ce8c8f2e7c65 languageName: node linkType: hard @@ -2099,17 +2108,17 @@ __metadata: languageName: unknown linkType: soft -"@standardnotes/models@npm:1.24.2": - version: 1.24.2 - resolution: "@standardnotes/models@npm:1.24.2" +"@standardnotes/models@npm:1.26.0, @standardnotes/models@npm:^1.26.0": + version: 1.26.0 + resolution: "@standardnotes/models@npm:1.26.0" dependencies: - "@standardnotes/common": ^1.36.1 - "@standardnotes/features": 1.52.4 - "@standardnotes/responses": 1.10.6 - "@standardnotes/utils": 1.9.1 + "@standardnotes/common": ^1.39.0 + "@standardnotes/features": 1.53.0 + "@standardnotes/responses": 1.11.0 + "@standardnotes/utils": 1.10.0 lodash: ^4.17.21 reflect-metadata: ^0.1.13 - checksum: 17b3cfba39c97f9e7b1960fe5cc0e6edd0ac0fbc492e266d7814972a6619465f8fb7ca6c90ac082de9592c83e2bbc2d883d9eaa076d4b283d93092e779a1af5c + checksum: f595a3de88ca815d04190ff188c1355ac1848369f94b81f7fc7be3a03918eaaa964eb078bb77622efd1ba7e2db1a47595db94e3ecc90c3544a54838a3980d6b0 languageName: node linkType: hard @@ -2138,15 +2147,15 @@ __metadata: languageName: unknown linkType: soft -"@standardnotes/responses@npm:1.10.6": - version: 1.10.6 - resolution: "@standardnotes/responses@npm:1.10.6" +"@standardnotes/responses@npm:1.11.0": + version: 1.11.0 + resolution: "@standardnotes/responses@npm:1.11.0" dependencies: - "@standardnotes/common": ^1.36.1 - "@standardnotes/features": 1.52.4 + "@standardnotes/common": ^1.39.0 + "@standardnotes/features": 1.53.0 "@standardnotes/security": ^1.1.0 reflect-metadata: ^0.1.13 - checksum: 0583e2cb77a23c22c7aa90e912c87b41fc8b8d236256cfa672e2a1272138974da0ebbbfa284049860adfed818e9c45a7f8ba169ff137b6b5c46a32a689320e6a + checksum: 46d6a479806507b15dc6f949e8863915f2cac81964aac715aab0bfc063e7bd7961767fcc6488e08397a0e1a52ccf2808429d8660b98a79415e25f601719125dc languageName: node linkType: hard @@ -2349,15 +2358,15 @@ __metadata: languageName: unknown linkType: soft -"@standardnotes/utils@npm:1.9.1": - version: 1.9.1 - resolution: "@standardnotes/utils@npm:1.9.1" +"@standardnotes/utils@npm:1.10.0": + version: 1.10.0 + resolution: "@standardnotes/utils@npm:1.10.0" dependencies: - "@standardnotes/common": ^1.36.1 + "@standardnotes/common": ^1.39.0 dompurify: ^2.3.8 lodash: ^4.17.21 reflect-metadata: ^0.1.13 - checksum: f775bb37447300bef1c02d911f7d132538bf52d8b65573129def50b0545b1a69af785b1d09932734ecc14a37e07cf1913e5e7b25866249a059bcea9fa7e9b18c + checksum: c02d54ca8a4debb62ecf6642825e1c313b3aad11767b1f0e6d36a976ec3058eba8592ab3194544042825296357676bc821e168489b638b0514bd1650dc9d0e0d languageName: node linkType: hard @@ -2378,10 +2387,11 @@ __metadata: dependencies: "@newrelic/winston-enricher": ^4.0.0 "@sentry/node": ^7.3.0 - "@standardnotes/api": ^1.12.1 + "@standardnotes/api": ^1.15.0 "@standardnotes/common": "workspace:*" "@standardnotes/domain-events": "workspace:^" "@standardnotes/domain-events-infra": "workspace:^" + "@standardnotes/models": ^1.26.0 "@standardnotes/security": "workspace:*" "@standardnotes/time": "workspace:^" "@types/cors": ^2.8.9