From abf0ccf83529358dfcbb237eac7d9b26a79bbad6 Mon Sep 17 00:00:00 2001 From: Alessandro Magionami Date: Wed, 22 Jan 2025 09:57:38 +0100 Subject: [PATCH] chore(workspaces): check user email verified and add user to workspace --- .../workspaces/graph/resolvers/workspaces.ts | 53 +++++------ .../services/workspaceJoinRequests.ts | 30 +++++- .../integration/workspaceJoinRequests.spec.ts | 91 +++++++++++++++---- 3 files changed, 128 insertions(+), 46 deletions(-) diff --git a/packages/server/modules/workspaces/graph/resolvers/workspaces.ts b/packages/server/modules/workspaces/graph/resolvers/workspaces.ts index 9b7eabf08..4facfb189 100644 --- a/packages/server/modules/workspaces/graph/resolvers/workspaces.ts +++ b/packages/server/modules/workspaces/graph/resolvers/workspaces.ts @@ -799,33 +799,34 @@ export = FF_WORKSPACES_MODULE_ENABLED })({ userId: ctx.userId!, workspaceId: args.input.workspaceId }) }, requestToJoin: async (_parent, args, ctx) => { - const transaction = await db.transaction() - const createWorkspaceJoinRequest = createWorkspaceJoinRequestFactory({ - db: transaction + const requestToJoin = commandFactory({ + db, + operationFactory: ({ db }) => { + const createWorkspaceJoinRequest = createWorkspaceJoinRequestFactory({ + db + }) + const sendWorkspaceJoinRequestReceivedEmail = + sendWorkspaceJoinRequestReceivedEmailFactory({ + renderEmail, + sendEmail, + getServerInfo, + getWorkspaceCollaborators: getWorkspaceCollaboratorsFactory({ + db + }), + getUserEmails: findEmailsByUserIdFactory({ db }) + }) + return requestToJoinWorkspaceFactory({ + createWorkspaceJoinRequest, + sendWorkspaceJoinRequestReceivedEmail, + getUserById: getUserFactory({ db }), + getWorkspace: getWorkspaceFactory({ db }) + }) + } + }) + return await requestToJoin({ + userId: ctx.userId!, + workspaceId: args.input.workspaceId }) - const sendWorkspaceJoinRequestReceivedEmail = - sendWorkspaceJoinRequestReceivedEmailFactory({ - renderEmail, - sendEmail, - getServerInfo, - getWorkspaceCollaborators: getWorkspaceCollaboratorsFactory({ - db: transaction - }), - getUserEmails: findEmailsByUserIdFactory({ db: transaction }) - }) - - return await withTransaction( - requestToJoinWorkspaceFactory({ - createWorkspaceJoinRequest, - sendWorkspaceJoinRequestReceivedEmail, - getUserById: getUserFactory({ db: transaction }), - getWorkspace: getWorkspaceFactory({ db: transaction }) - })({ - userId: ctx.userId!, - workspaceId: args.input.workspaceId - }), - transaction - ) } }, WorkspaceInviteMutations: { diff --git a/packages/server/modules/workspaces/services/workspaceJoinRequests.ts b/packages/server/modules/workspaces/services/workspaceJoinRequests.ts index 4a0019908..22afcd8a4 100644 --- a/packages/server/modules/workspaces/services/workspaceJoinRequests.ts +++ b/packages/server/modules/workspaces/services/workspaceJoinRequests.ts @@ -1,4 +1,9 @@ -import { WorkspaceNotFoundError } from '@/modules/workspaces/errors/workspace' +import { + WorkspaceJoinNotAllowedError, + WorkspaceNotDiscoverableError, + WorkspaceNotFoundError, + WorkspaceNotJoinableError +} from '@/modules/workspaces/errors/workspace' import { GetUser } from '@/modules/core/domain/users/operations' import { NotFoundError } from '@/modules/shared/errors' import { @@ -6,6 +11,7 @@ import { DenyWorkspaceJoinRequest, GetWorkspace, GetWorkspaceJoinRequest, + GetWorkspaceWithDomains, SendWorkspaceJoinRequestApprovedEmail, SendWorkspaceJoinRequestDeniedEmail, SendWorkspaceJoinRequestReceivedEmail, @@ -13,6 +19,7 @@ import { UpsertWorkspaceRole } from '@/modules/workspaces/domain/operations' import { Roles } from '@speckle/shared' +import { FindEmailsByUserId } from '@/modules/core/domain/userEmails/operations' export const dismissWorkspaceJoinRequestFactory = ({ @@ -40,12 +47,14 @@ export const requestToJoinWorkspaceFactory = createWorkspaceJoinRequest, sendWorkspaceJoinRequestReceivedEmail, getUserById, - getWorkspace + getWorkspaceWithDomains, + getUserEmails }: { createWorkspaceJoinRequest: CreateWorkspaceJoinRequest sendWorkspaceJoinRequestReceivedEmail: SendWorkspaceJoinRequestReceivedEmail getUserById: GetUser - getWorkspace: GetWorkspace + getWorkspaceWithDomains: GetWorkspaceWithDomains + getUserEmails: FindEmailsByUserId }) => async ({ userId, workspaceId }: { userId: string; workspaceId: string }) => { const requester = await getUserById(userId) @@ -53,10 +62,23 @@ export const requestToJoinWorkspaceFactory = throw new NotFoundError('User not found') } - const workspace = await getWorkspace({ workspaceId }) + const workspace = await getWorkspaceWithDomains({ id: workspaceId }) if (!workspace) { throw new WorkspaceNotFoundError('Workspace not found') } + if (!workspace?.discoverabilityEnabled) throw new WorkspaceNotDiscoverableError() + const workspaceDomains = workspace.domains.filter((domain) => domain.verified) + if (!workspaceDomains.length) throw new WorkspaceNotJoinableError() + + const userEmails = await getUserEmails({ userId }) + const matchingEmail = userEmails.find((userEmail) => { + if (!userEmail.verified) return false + return workspaceDomains + .map((domain) => domain.domain) + .includes(userEmail.email.split('@')[1]) + }) + + if (!matchingEmail) throw new WorkspaceJoinNotAllowedError() await createWorkspaceJoinRequest({ workspaceJoinRequest: { diff --git a/packages/server/modules/workspaces/tests/integration/workspaceJoinRequests.spec.ts b/packages/server/modules/workspaces/tests/integration/workspaceJoinRequests.spec.ts index 3b8d47b47..44d1cabd5 100644 --- a/packages/server/modules/workspaces/tests/integration/workspaceJoinRequests.spec.ts +++ b/packages/server/modules/workspaces/tests/integration/workspaceJoinRequests.spec.ts @@ -4,7 +4,10 @@ import { createRandomString } from '@/modules/core/helpers/testHelpers' import { getFeatureFlags } from '@/modules/shared/helpers/envHelper' -import { WorkspaceNotFoundError } from '@/modules/workspaces/errors/workspace' +import { + WorkspaceNotDiscoverableError, + WorkspaceNotFoundError +} from '@/modules/workspaces/errors/workspace' import { getWorkspaceFactory } from '@/modules/workspaces/repositories/workspaces' import { UserWithOptionalRole } from '@/modules/core/repositories/users' import { @@ -25,7 +28,11 @@ import { BasicTestWorkspace, createTestWorkspace } from '@/modules/workspaces/tests/helpers/creation' -import { Workspace, WorkspaceJoinRequest } from '@/modules/workspacesCore/domain/types' +import { + Workspace, + WorkspaceJoinRequest, + WorkspaceWithDomains +} from '@/modules/workspacesCore/domain/types' import { WorkspaceJoinRequests } from '@/modules/workspacesCore/helpers/db' import { expectToThrow } from '@/test/assertionHelper' import { BasicTestUser, createTestUser } from '@/test/authHelper' @@ -36,6 +43,7 @@ import { createWorkspaceJoinRequestFactory, updateWorkspaceJoinRequestStatusFactory } from '@/modules/workspaces/repositories/workspaceJoinRequests' +import { UserEmail } from '@/modules/core/domain/userEmails/types' const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags() @@ -100,7 +108,8 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags() Promise.resolve()) as unknown as CreateWorkspaceJoinRequest, sendWorkspaceJoinRequestReceivedEmail: async () => Promise.resolve(), getUserById: async () => null, - getWorkspace: async () => null + getWorkspaceWithDomains: async () => null, + getUserEmails: async () => [] })({ workspaceId: createRandomString(), userId: createRandomString() }) ) @@ -114,21 +123,14 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags() Promise.resolve()) as unknown as CreateWorkspaceJoinRequest, sendWorkspaceJoinRequestReceivedEmail: async () => Promise.resolve(), getUserById: async () => user as unknown as UserWithOptionalRole, - getWorkspace: async () => null + getWorkspaceWithDomains: async () => null, + getUserEmails: async () => [] })({ workspaceId: createRandomString(), userId: createRandomString() }) ) expect(err.message).to.equal(WorkspaceNotFoundError.defaultMessage) }) - it('creates a join request and sends an email to all admins', async () => { - const createWorkspaceJoinRequest = createWorkspaceJoinRequestFactory({ db }) - - const sendWorkspaceJoinRequestReceivedEmailCalls: Parameters[number][] = - [] - const sendWorkspaceJoinRequestReceivedEmail = async ( - args: Parameters[number] - ) => sendWorkspaceJoinRequestReceivedEmailCalls.push(args) - + it('throws a WorkspaceNotDiscoverable if the workspace has no domain', async () => { const user: BasicTestUser = { id: '', name: 'John Speckle', @@ -147,6 +149,57 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags() description: cryptoRandomString({ length: 12 }) } await createTestWorkspace(workspace, user) + const err = await expectToThrow(() => + requestToJoinWorkspaceFactory({ + createWorkspaceJoinRequest: (async () => + Promise.resolve()) as unknown as CreateWorkspaceJoinRequest, + sendWorkspaceJoinRequestReceivedEmail: async () => Promise.resolve(), + getUserById: async () => user as unknown as UserWithOptionalRole, + getWorkspaceWithDomains: async () => + workspace as unknown as WorkspaceWithDomains, + getUserEmails: async () => [] + })({ workspaceId: createRandomString(), userId: createRandomString() }) + ) + + expect(err.message).to.equal(WorkspaceNotDiscoverableError.defaultMessage) + }) + it('creates a join request and sends an email to all admins', async () => { + const createWorkspaceJoinRequest = createWorkspaceJoinRequestFactory({ db }) + + const sendWorkspaceJoinRequestReceivedEmailCalls: Parameters[number][] = + [] + const sendWorkspaceJoinRequestReceivedEmail = async ( + args: Parameters[number] + ) => sendWorkspaceJoinRequestReceivedEmailCalls.push(args) + + const user: BasicTestUser = { + id: '', + name: 'John Speckle', + email: `${createRandomString()}@example.org`, + role: Roles.Server.Admin, + verified: true + } + + await createTestUser(user) + + const workspace: BasicTestWorkspace = { + id: '', + slug: '', + ownerId: '', + name: cryptoRandomString({ length: 6 }), + description: cryptoRandomString({ length: 12 }), + discoverabilityEnabled: true + } + await createTestWorkspace(workspace, user, { domain: 'example.org' }) + const domain = { + id: createRandomString(), + workspaceId: workspace.id, + domain: 'example.org', + verified: true, + createdAt: new Date(), + createdByUserId: user.id, + updatedAt: new Date() + } expect( await requestToJoinWorkspaceFactory({ @@ -154,7 +207,13 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags() sendWorkspaceJoinRequestReceivedEmail: sendWorkspaceJoinRequestReceivedEmail as unknown as SendWorkspaceJoinRequestReceivedEmail, getUserById: async () => user as unknown as UserWithOptionalRole, - getWorkspace: async () => workspace as unknown as Workspace + getWorkspaceWithDomains: async () => + ({ + ...workspace, + domains: [domain] + } as unknown as WorkspaceWithDomains), + getUserEmails: async () => + [{ email: user.email, verified: true }] as unknown as UserEmail[] })({ workspaceId: workspace.id, userId: user.id }) ).to.equal(true) @@ -169,8 +228,8 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags() ).to.equal('pending') expect(sendWorkspaceJoinRequestReceivedEmailCalls).to.have.length(1) - expect(sendWorkspaceJoinRequestReceivedEmailCalls[0].workspace).to.equal( - workspace + expect(sendWorkspaceJoinRequestReceivedEmailCalls[0].workspace.id).to.equal( + workspace.id ) expect(sendWorkspaceJoinRequestReceivedEmailCalls[0].requester).to.equal(user) })