From 3a197792b3aebe047797987e8fa5a15cf2916d65 Mon Sep 17 00:00:00 2001 From: Alessandro Magionami Date: Tue, 21 Jan 2025 17:03:23 +0100 Subject: [PATCH 1/6] chore(workspaces): create id resolver for workspace join request --- .../server/assets/workspacesCore/typedefs/workspaces.graphql | 1 + packages/server/modules/core/graph/generated/graphql.ts | 2 ++ .../modules/cross-server-sync/graph/generated/graphql.ts | 1 + .../workspaces/graph/resolvers/workspaceJoinRequests.ts | 3 +++ packages/server/test/graphql/generated/graphql.ts | 1 + 5 files changed, 8 insertions(+) diff --git a/packages/server/assets/workspacesCore/typedefs/workspaces.graphql b/packages/server/assets/workspacesCore/typedefs/workspaces.graphql index fc71d2b96..c20a1c09f 100644 --- a/packages/server/assets/workspacesCore/typedefs/workspaces.graphql +++ b/packages/server/assets/workspacesCore/typedefs/workspaces.graphql @@ -468,6 +468,7 @@ type WorkspaceJoinRequestCollection { } type WorkspaceJoinRequest { + id: String! workspace: Workspace! user: LimitedUser! status: WorkspaceJoinRequestStatus! diff --git a/packages/server/modules/core/graph/generated/graphql.ts b/packages/server/modules/core/graph/generated/graphql.ts index e53626aca..13f7486d0 100644 --- a/packages/server/modules/core/graph/generated/graphql.ts +++ b/packages/server/modules/core/graph/generated/graphql.ts @@ -4423,6 +4423,7 @@ export type WorkspaceInviteUseInput = { export type WorkspaceJoinRequest = { __typename?: 'WorkspaceJoinRequest'; createdAt: Scalars['DateTime']['output']; + id: Scalars['String']['output']; status: WorkspaceJoinRequestStatus; user: LimitedUser; workspace: Workspace; @@ -6886,6 +6887,7 @@ export type WorkspaceInviteMutationsResolvers = { createdAt?: Resolver; + id?: Resolver; status?: Resolver; user?: Resolver; workspace?: Resolver; diff --git a/packages/server/modules/cross-server-sync/graph/generated/graphql.ts b/packages/server/modules/cross-server-sync/graph/generated/graphql.ts index d01b5ffe7..62107b8d0 100644 --- a/packages/server/modules/cross-server-sync/graph/generated/graphql.ts +++ b/packages/server/modules/cross-server-sync/graph/generated/graphql.ts @@ -4404,6 +4404,7 @@ export type WorkspaceInviteUseInput = { export type WorkspaceJoinRequest = { __typename?: 'WorkspaceJoinRequest'; createdAt: Scalars['DateTime']['output']; + id: Scalars['String']['output']; status: WorkspaceJoinRequestStatus; user: LimitedUser; workspace: Workspace; diff --git a/packages/server/modules/workspaces/graph/resolvers/workspaceJoinRequests.ts b/packages/server/modules/workspaces/graph/resolvers/workspaceJoinRequests.ts index c9a7464b1..c48624b55 100644 --- a/packages/server/modules/workspaces/graph/resolvers/workspaceJoinRequests.ts +++ b/packages/server/modules/workspaces/graph/resolvers/workspaceJoinRequests.ts @@ -58,6 +58,9 @@ export default { } }, WorkspaceJoinRequest: { + id: async (parent) => { + return parent.userId + parent.workspaceId + }, user: async (parent, _args, ctx) => { return await ctx.loaders.users.getUser.load(parent.userId) }, diff --git a/packages/server/test/graphql/generated/graphql.ts b/packages/server/test/graphql/generated/graphql.ts index 091cdd433..1195f0a5a 100644 --- a/packages/server/test/graphql/generated/graphql.ts +++ b/packages/server/test/graphql/generated/graphql.ts @@ -4405,6 +4405,7 @@ export type WorkspaceInviteUseInput = { export type WorkspaceJoinRequest = { __typename?: 'WorkspaceJoinRequest'; createdAt: Scalars['DateTime']['output']; + id: Scalars['String']['output']; status: WorkspaceJoinRequestStatus; user: LimitedUser; workspace: Workspace; From adaad0d0277c1e690ce4bd775a7b4fdeb8b4264c Mon Sep 17 00:00:00 2001 From: Alessandro Magionami Date: Tue, 21 Jan 2025 17:17:22 +0100 Subject: [PATCH 2/6] chore(workspaces): add user to workspace on approved request --- .../graph/resolvers/workspaceJoinRequests.ts | 8 ++++-- .../services/workspaceJoinRequests.ts | 11 ++++++-- .../integration/workspaceJoinRequests.spec.ts | 27 +++++++++++++++---- 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/packages/server/modules/workspaces/graph/resolvers/workspaceJoinRequests.ts b/packages/server/modules/workspaces/graph/resolvers/workspaceJoinRequests.ts index c48624b55..0956e5369 100644 --- a/packages/server/modules/workspaces/graph/resolvers/workspaceJoinRequests.ts +++ b/packages/server/modules/workspaces/graph/resolvers/workspaceJoinRequests.ts @@ -17,7 +17,10 @@ import { getWorkspaceJoinRequestFactory, updateWorkspaceJoinRequestStatusFactory } from '@/modules/workspaces/repositories/workspaceJoinRequests' -import { getWorkspaceFactory } from '@/modules/workspaces/repositories/workspaces' +import { + getWorkspaceFactory, + upsertWorkspaceRoleFactory +} from '@/modules/workspaces/repositories/workspaces' import { sendWorkspaceJoinRequestApprovedEmailFactory } from '@/modules/workspaces/services/workspaceJoinRequestEmails/approved' import { sendWorkspaceJoinRequestDeniedEmailFactory } from '@/modules/workspaces/services/workspaceJoinRequestEmails/denied' import { @@ -94,7 +97,8 @@ export default { getWorkspace: getWorkspaceFactory({ db }), getWorkspaceJoinRequest: getWorkspaceJoinRequestFactory({ db - }) + }), + upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db }) }) } }) diff --git a/packages/server/modules/workspaces/services/workspaceJoinRequests.ts b/packages/server/modules/workspaces/services/workspaceJoinRequests.ts index 004709269..4a0019908 100644 --- a/packages/server/modules/workspaces/services/workspaceJoinRequests.ts +++ b/packages/server/modules/workspaces/services/workspaceJoinRequests.ts @@ -9,8 +9,10 @@ import { SendWorkspaceJoinRequestApprovedEmail, SendWorkspaceJoinRequestDeniedEmail, SendWorkspaceJoinRequestReceivedEmail, - UpdateWorkspaceJoinRequestStatus + UpdateWorkspaceJoinRequestStatus, + UpsertWorkspaceRole } from '@/modules/workspaces/domain/operations' +import { Roles } from '@speckle/shared' export const dismissWorkspaceJoinRequestFactory = ({ @@ -78,13 +80,15 @@ export const approveWorkspaceJoinRequestFactory = sendWorkspaceJoinRequestApprovedEmail, getUserById, getWorkspace, - getWorkspaceJoinRequest + getWorkspaceJoinRequest, + upsertWorkspaceRole }: { updateWorkspaceJoinRequestStatus: UpdateWorkspaceJoinRequestStatus sendWorkspaceJoinRequestApprovedEmail: SendWorkspaceJoinRequestApprovedEmail getUserById: GetUser getWorkspace: GetWorkspace getWorkspaceJoinRequest: GetWorkspaceJoinRequest + upsertWorkspaceRole: UpsertWorkspaceRole }) => async ({ userId, workspaceId }: { userId: string; workspaceId: string }) => { const requester = await getUserById(userId) @@ -112,6 +116,9 @@ export const approveWorkspaceJoinRequestFactory = status: 'approved' }) + const role = Roles.Workspace.Member + await upsertWorkspaceRole({ userId, workspaceId, role, createdAt: new Date() }) + await sendWorkspaceJoinRequestApprovedEmail({ workspace, requester diff --git a/packages/server/modules/workspaces/tests/integration/workspaceJoinRequests.spec.ts b/packages/server/modules/workspaces/tests/integration/workspaceJoinRequests.spec.ts index f9b5e9a41..3b8d47b47 100644 --- a/packages/server/modules/workspaces/tests/integration/workspaceJoinRequests.spec.ts +++ b/packages/server/modules/workspaces/tests/integration/workspaceJoinRequests.spec.ts @@ -12,7 +12,8 @@ import { SendWorkspaceJoinRequestApprovedEmail, SendWorkspaceJoinRequestDeniedEmail, SendWorkspaceJoinRequestReceivedEmail, - UpdateWorkspaceJoinRequestStatus + UpdateWorkspaceJoinRequestStatus, + UpsertWorkspaceRole } from '@/modules/workspaces/domain/operations' import { denyWorkspaceJoinRequestFactory, @@ -184,7 +185,8 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags() sendWorkspaceJoinRequestApprovedEmail: async () => Promise.resolve(), getUserById: async () => null, getWorkspace: async () => null, - getWorkspaceJoinRequest: async () => undefined + getWorkspaceJoinRequest: async () => undefined, + upsertWorkspaceRole: async () => Promise.resolve() })({ workspaceId: createRandomString(), userId: createRandomString() }) ) @@ -199,7 +201,8 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags() sendWorkspaceJoinRequestApprovedEmail: async () => Promise.resolve(), getUserById: async () => user as unknown as UserWithOptionalRole, getWorkspace: async () => null, - getWorkspaceJoinRequest: async () => undefined + getWorkspaceJoinRequest: async () => undefined, + upsertWorkspaceRole: async () => Promise.resolve() })({ workspaceId: createRandomString(), userId: createRandomString() }) ) @@ -222,7 +225,8 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags() sendWorkspaceJoinRequestApprovedEmail: async () => Promise.resolve(), getUserById: async () => user as unknown as UserWithOptionalRole, getWorkspace: async () => workspace as unknown as Workspace, - getWorkspaceJoinRequest: async () => undefined + getWorkspaceJoinRequest: async () => undefined, + upsertWorkspaceRole: async () => Promise.resolve() })({ workspaceId: createRandomString(), userId: createRandomString() }) ) @@ -235,6 +239,13 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags() args: Parameters[number] ) => sendWorkspaceJoinRequestApprovedEmailCalls.push(args) + const upsertWorkspaceRoleCalls: Parameters[number][] = [] + const upsertWorkspaceRole = async ( + args: Parameters[number] + ) => { + upsertWorkspaceRoleCalls.push(args) + } + const user: BasicTestUser = { id: '', name: 'John Speckle', @@ -272,7 +283,8 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags() sendWorkspaceJoinRequestApprovedEmail as unknown as SendWorkspaceJoinRequestApprovedEmail, getUserById: async () => user as unknown as UserWithOptionalRole, getWorkspace: async () => workspace as unknown as Workspace, - getWorkspaceJoinRequest: async () => request + getWorkspaceJoinRequest: async () => request, + upsertWorkspaceRole })({ workspaceId: workspace.id, userId: user.id }) ).to.equal(true) @@ -286,6 +298,11 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags() .first())!.status ).to.equal('approved') + expect(upsertWorkspaceRoleCalls).to.have.length(1) + expect(upsertWorkspaceRoleCalls[0].workspaceId).to.equal(workspace.id) + expect(upsertWorkspaceRoleCalls[0].userId).to.equal(user.id) + expect(upsertWorkspaceRoleCalls[0].role).to.equal(Roles.Workspace.Member) + expect(sendWorkspaceJoinRequestApprovedEmailCalls).to.have.length(1) expect(sendWorkspaceJoinRequestApprovedEmailCalls[0].workspace).to.equal( workspace From abf0ccf83529358dfcbb237eac7d9b26a79bbad6 Mon Sep 17 00:00:00 2001 From: Alessandro Magionami Date: Wed, 22 Jan 2025 09:57:38 +0100 Subject: [PATCH 3/6] 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) }) From 9973894b7dad3ccf43b66353909a66674f13c247 Mon Sep 17 00:00:00 2001 From: Alessandro Magionami Date: Wed, 22 Jan 2025 10:33:39 +0100 Subject: [PATCH 4/6] chore(workspaces): fix resolver --- .../server/modules/workspaces/graph/resolvers/workspaces.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/server/modules/workspaces/graph/resolvers/workspaces.ts b/packages/server/modules/workspaces/graph/resolvers/workspaces.ts index 4facfb189..e6d5bc75c 100644 --- a/packages/server/modules/workspaces/graph/resolvers/workspaces.ts +++ b/packages/server/modules/workspaces/graph/resolvers/workspaces.ts @@ -819,7 +819,8 @@ export = FF_WORKSPACES_MODULE_ENABLED createWorkspaceJoinRequest, sendWorkspaceJoinRequestReceivedEmail, getUserById: getUserFactory({ db }), - getWorkspace: getWorkspaceFactory({ db }) + getWorkspaceWithDomains: getWorkspaceWithDomainsFactory({ db }), + getUserEmails: findEmailsByUserIdFactory({ db }) }) } }) From 37cf9f0281149bc3262b31971696c2cc9c3a90c9 Mon Sep 17 00:00:00 2001 From: Alessandro Magionami Date: Wed, 22 Jan 2025 11:10:23 +0100 Subject: [PATCH 5/6] chore(workspaces): fix request to join test --- .../workspaceJoinRequests.graph.spec.ts | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/packages/server/modules/workspaces/tests/integration/workspaceJoinRequests.graph.spec.ts b/packages/server/modules/workspaces/tests/integration/workspaceJoinRequests.graph.spec.ts index ec10b513b..2fcaba4de 100644 --- a/packages/server/modules/workspaces/tests/integration/workspaceJoinRequests.graph.spec.ts +++ b/packages/server/modules/workspaces/tests/integration/workspaceJoinRequests.graph.spec.ts @@ -38,43 +38,59 @@ describe('WorkspaceJoinRequests GQL', () => { it('should return the workspace join requests for the admin', async () => { const admin = await createTestUser({ name: 'admin user', - role: Roles.Server.User + role: Roles.Server.User, + email: `${createRandomString()}@example.org`, + verified: true }) - const user1 = await createTestUser({ name: 'user 1', role: Roles.Server.User }) - const user2 = await createTestUser({ name: 'user 2', role: Roles.Server.User }) + const user1 = await createTestUser({ + name: 'user 1', + role: Roles.Server.User, + email: `${createRandomString()}@example.org`, + verified: true + }) + const user2 = await createTestUser({ + name: 'user 2', + role: Roles.Server.User, + email: `${createRandomString()}@example.org`, + verified: true + }) const workspace1 = { id: createRandomString(), name: 'Workspace 1', ownerId: admin.id, - description: '' + description: '', + discoverabilityEnabled: true } - await createTestWorkspace(workspace1, admin) + await createTestWorkspace(workspace1, admin, { domain: 'example.org' }) const workspace2 = { id: createRandomString(), name: 'Workspace 2', ownerId: admin.id, - description: '' + description: '', + discoverabilityEnabled: true } - await createTestWorkspace(workspace2, admin) + await createTestWorkspace(workspace2, admin, { domain: 'example.org' }) const nobodyWorkspace = { id: createRandomString(), name: 'nobody', ownerId: admin.id, - description: '' + description: '', + discoverabilityEnabled: true } - await createTestWorkspace(nobodyWorkspace, admin) + await createTestWorkspace(nobodyWorkspace, admin, { domain: 'example.org' }) const nonAdminWorkspace = { id: createRandomString(), name: 'nonadmin', ownerId: admin.id, - description: '' + description: '', + discoverabilityEnabled: true } - await createTestWorkspace(nonAdminWorkspace, admin) + await createTestWorkspace(nonAdminWorkspace, admin, { domain: 'example.org' }) await upsertWorkspaceRoleFactory({ db })({ userId: admin.id, workspaceId: nonAdminWorkspace.id, From 17c8ada36959deb646fba7eb435fbe65696bd964 Mon Sep 17 00:00:00 2001 From: Alessandro Magionami Date: Wed, 22 Jan 2025 12:08:36 +0100 Subject: [PATCH 6/6] chore(workspaces): use logic function to check workspace is joinable --- .../services/workspaceJoinRequests.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/server/modules/workspaces/services/workspaceJoinRequests.ts b/packages/server/modules/workspaces/services/workspaceJoinRequests.ts index 22afcd8a4..807cd1c2e 100644 --- a/packages/server/modules/workspaces/services/workspaceJoinRequests.ts +++ b/packages/server/modules/workspaces/services/workspaceJoinRequests.ts @@ -1,8 +1,8 @@ import { - WorkspaceJoinNotAllowedError, WorkspaceNotDiscoverableError, WorkspaceNotFoundError, - WorkspaceNotJoinableError + WorkspaceNotJoinableError, + WorkspaceProtectedError } from '@/modules/workspaces/errors/workspace' import { GetUser } from '@/modules/core/domain/users/operations' import { NotFoundError } from '@/modules/shared/errors' @@ -20,6 +20,7 @@ import { } from '@/modules/workspaces/domain/operations' import { Roles } from '@speckle/shared' import { FindEmailsByUserId } from '@/modules/core/domain/userEmails/operations' +import { userEmailsCompliantWithWorkspaceDomains } from '@/modules/workspaces/domain/logic' export const dismissWorkspaceJoinRequestFactory = ({ @@ -71,14 +72,14 @@ export const requestToJoinWorkspaceFactory = 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() + const canJoinWorkspace = userEmailsCompliantWithWorkspaceDomains({ + workspaceDomains: workspace.domains, + userEmails + }) + if (!canJoinWorkspace) { + throw new WorkspaceProtectedError() + } await createWorkspaceJoinRequest({ workspaceJoinRequest: {