From ab1ad658585a38c0eb3cf7a6f667c0a2dd06ac27 Mon Sep 17 00:00:00 2001 From: DarkSky <25152247+darkskygit@users.noreply.github.com> Date: Thu, 12 Dec 2024 20:32:32 +0800 Subject: [PATCH] feat(server): adapt normal workspace's invite link behavior (#9130) --- .../core/workspaces/resolvers/workspace.ts | 49 +++++++---- packages/backend/server/tests/team.e2e.ts | 88 +++++++++++++------ packages/backend/server/tests/utils/invite.ts | 1 - 3 files changed, 92 insertions(+), 46 deletions(-) diff --git a/packages/backend/server/src/core/workspaces/resolvers/workspace.ts b/packages/backend/server/src/core/workspaces/resolvers/workspace.ts index 7f72993992..b374e17cc0 100644 --- a/packages/backend/server/src/core/workspaces/resolvers/workspace.ts +++ b/packages/backend/server/src/core/workspaces/resolvers/workspace.ts @@ -22,6 +22,7 @@ import { EventEmitter, InternalServerError, MailService, + MemberQuotaExceeded, RequestMutex, SpaceAccessDenied, SpaceNotFound, @@ -528,13 +529,14 @@ export class WorkspaceResolver { return new TooManyRequest(); } + const isTeam = await this.quota.isTeamWorkspace(workspaceId); if (user) { const status = await this.permissions.getWorkspaceMemberStatus( workspaceId, user.id ); if (status === WorkspaceMemberStatus.Accepted) { - throw new AlreadyInSpace({ spaceId: workspaceId }); + return new AlreadyInSpace({ spaceId: workspaceId }); } // invite link @@ -544,35 +546,44 @@ export class WorkspaceResolver { if (invite?.inviteId === inviteId) { const quota = await this.quota.getWorkspaceUsage(workspaceId); if (quota.memberCount >= quota.memberLimit) { - await this.permissions.grant( - workspaceId, - user.id, - Permission.Write, - WorkspaceMemberStatus.NeedMoreSeatAndReview - ); - const memberCount = - await this.permissions.getWorkspaceMemberCount(workspaceId); - this.event.emit('workspace.members.updated', { - workspaceId, - count: memberCount, - }); - return true; + // only team workspace allow over limit + if (isTeam) { + await this.permissions.grant( + workspaceId, + user.id, + Permission.Write, + WorkspaceMemberStatus.NeedMoreSeatAndReview + ); + const memberCount = + await this.permissions.getWorkspaceMemberCount(workspaceId); + this.event.emit('workspace.members.updated', { + workspaceId, + count: memberCount, + }); + return true; + } else if (!status) { + return new MemberQuotaExceeded(); + } } else { const inviteId = await this.permissions.grant(workspaceId, user.id); - this.event.emit('workspace.team.reviewRequest', { - inviteIds: [inviteId], - }); + if (isTeam) { + this.event.emit('workspace.team.reviewRequest', { + inviteIds: [inviteId], + }); + } // invite by link need admin to approve return await this.permissions.acceptWorkspaceInvitation( inviteId, workspaceId, - WorkspaceMemberStatus.UnderReview + isTeam + ? WorkspaceMemberStatus.UnderReview + : WorkspaceMemberStatus.Accepted ); } } } - // we added seats when sending invitation emails, but the deduction may fail + // we added seats when sending invitation emails, but the payment may fail // so we need to check seat again here await this.quota.checkWorkspaceSeat(workspaceId, true); diff --git a/packages/backend/server/tests/team.e2e.ts b/packages/backend/server/tests/team.e2e.ts index 6c85226689..e48d3f96f4 100644 --- a/packages/backend/server/tests/team.e2e.ts +++ b/packages/backend/server/tests/team.e2e.ts @@ -14,6 +14,7 @@ import { QuotaService, QuotaType, } from '../src/core/quota'; +import { WorkspaceType } from '../src/core/workspaces'; import { acceptInviteById, createInviteLink, @@ -71,11 +72,12 @@ test.afterEach.always(async t => { const init = async (app: INestApplication, memberLimit = 10) => { const owner = await signUp(app, 'test', 'test@affine.pro', '123456'); - const ws = await createWorkspace(app, owner.token.token); + const workspace = await createWorkspace(app, owner.token.token); + const teamWorkspace = await createWorkspace(app, owner.token.token); const quota = app.get(QuotaManagementService); - await quota.addTeamWorkspace(ws.id, 'test'); - await quota.updateWorkspaceConfig(ws.id, QuotaType.TeamPlanV1, { + await quota.addTeamWorkspace(teamWorkspace.id, 'test'); + await quota.updateWorkspaceConfig(teamWorkspace.id, QuotaType.TeamPlanV1, { memberLimit, }); @@ -87,11 +89,11 @@ const init = async (app: INestApplication, memberLimit = 10) => { const inviteId = await inviteUser( app, owner.token.token, - ws.id, + teamWorkspace.id, member.email, permission ); - await acceptInviteById(app, ws.id, inviteId); + await acceptInviteById(app, teamWorkspace.id, inviteId); return member; }; @@ -101,11 +103,16 @@ const init = async (app: INestApplication, memberLimit = 10) => { const member = await signUp(app, email.split('@')[0], email, '123456'); members.push(member); } - const invites = await inviteUsers(app, owner.token.token, ws.id, emails); + const invites = await inviteUsers( + app, + owner.token.token, + teamWorkspace.id, + emails + ); return [members, invites] as const; }; - const getCreateInviteLinkFetcher = async () => { + const getCreateInviteLinkFetcher = async (ws: WorkspaceType) => { const { link } = await createInviteLink( app, owner.token.token, @@ -135,7 +142,8 @@ const init = async (app: INestApplication, memberLimit = 10) => { inviteBatch, createInviteLink: getCreateInviteLinkFetcher, owner, - ws, + workspace, + teamWorkspace, admin, write, read, @@ -144,7 +152,7 @@ const init = async (app: INestApplication, memberLimit = 10) => { test('should be able to check seat limit', async t => { const { app, permissions, quotaManager } = t.context; - const { invite, inviteBatch, ws } = await init(app, 4); + const { invite, inviteBatch, teamWorkspace: ws } = await init(app, 4); { // invite @@ -202,7 +210,7 @@ test('should be able to check seat limit', async t => { test('should be able to grant team member permission', async t => { const { app, permissions } = t.context; - const { owner, ws, admin, write, read } = await init(app); + const { owner, teamWorkspace: ws, admin, write, read } = await init(app); await t.throwsAsync( grantMember(app, read.token.token, ws.id, write.id, 'Write'), @@ -239,7 +247,7 @@ test('should be able to grant team member permission', async t => { test('should be able to leave workspace', async t => { const { app } = t.context; - const { owner, ws, admin, write, read } = await init(app); + const { owner, teamWorkspace: ws, admin, write, read } = await init(app); t.false( await leaveWorkspace(app, owner.token.token, ws.id), @@ -261,64 +269,92 @@ test('should be able to leave workspace', async t => { test('should be able to invite by link', async t => { const { app, permissions, quotaManager } = t.context; - const { createInviteLink, owner, ws } = await init(app, 4); - const [inviteId, invite, acceptInvite] = await createInviteLink(); + const { + createInviteLink, + owner, + workspace: ws, + teamWorkspace: tws, + } = await init(app, 4); + const [inviteId, invite] = await createInviteLink(ws); + const [teamInviteId, teamInvite, acceptTeamInvite] = + await createInviteLink(tws); { // check invite link const info = await getInviteInfo(app, owner.token.token, inviteId); t.is(info.workspace.id, ws.id, 'should be able to get invite info'); + + // check team invite link + const teamInfo = await getInviteInfo(app, owner.token.token, teamInviteId); + t.is(teamInfo.workspace.id, tws.id, 'should be able to get invite info'); } { // invite link + const t1 = await invite('test1@affine.pro'); + const t2 = await invite('test2@affine.pro'); + + await t.throwsAsync( + invite('test3@affine.pro'), + { message: 'You have exceeded your workspace member quota.' }, + 'should throw error if exceed member limit' + ); + + const s1 = await permissions.getWorkspaceMemberStatus(ws.id, t1.id); + t.is(s1, WorkspaceMemberStatus.Accepted, 'should be able to check status'); + const s2 = await permissions.getWorkspaceMemberStatus(ws.id, t2.id); + t.is(s2, WorkspaceMemberStatus.Accepted, 'should be able to check status'); + } + + { + // team invite link const members: UserAuthedType[] = []; await t.notThrowsAsync(async () => { - members.push(await invite('member3@affine.pro')); - members.push(await invite('member4@affine.pro')); + members.push(await teamInvite('member3@affine.pro')); + members.push(await teamInvite('member4@affine.pro')); }, 'should not throw error even exceed member limit'); const [m3, m4] = members; t.is( - await permissions.getWorkspaceMemberStatus(ws.id, m3.id), + await permissions.getWorkspaceMemberStatus(tws.id, m3.id), WorkspaceMemberStatus.NeedMoreSeatAndReview, 'should not change status' ); t.is( - await permissions.getWorkspaceMemberStatus(ws.id, m4.id), + await permissions.getWorkspaceMemberStatus(tws.id, m4.id), WorkspaceMemberStatus.NeedMoreSeatAndReview, 'should not change status' ); - await quotaManager.updateWorkspaceConfig(ws.id, QuotaType.TeamPlanV1, { + await quotaManager.updateWorkspaceConfig(tws.id, QuotaType.TeamPlanV1, { memberLimit: 5, }); - await permissions.refreshSeatStatus(ws.id, 5); + await permissions.refreshSeatStatus(tws.id, 5); t.is( - await permissions.getWorkspaceMemberStatus(ws.id, m3.id), + await permissions.getWorkspaceMemberStatus(tws.id, m3.id), WorkspaceMemberStatus.UnderReview, 'should not change status' ); t.is( - await permissions.getWorkspaceMemberStatus(ws.id, m4.id), + await permissions.getWorkspaceMemberStatus(tws.id, m4.id), WorkspaceMemberStatus.NeedMoreSeatAndReview, 'should not change status' ); - await quotaManager.updateWorkspaceConfig(ws.id, QuotaType.TeamPlanV1, { + await quotaManager.updateWorkspaceConfig(tws.id, QuotaType.TeamPlanV1, { memberLimit: 6, }); - await permissions.refreshSeatStatus(ws.id, 6); + await permissions.refreshSeatStatus(tws.id, 6); t.is( - await permissions.getWorkspaceMemberStatus(ws.id, m4.id), + await permissions.getWorkspaceMemberStatus(tws.id, m4.id), WorkspaceMemberStatus.UnderReview, 'should not change status' ); { - const message = `You have already joined in Space ${ws.id}.`; + const message = `You have already joined in Space ${tws.id}.`; await t.throwsAsync( - acceptInvite(owner.token.token), + acceptTeamInvite(owner.token.token), { message }, 'should throw error if member already in workspace' ); diff --git a/packages/backend/server/tests/utils/invite.ts b/packages/backend/server/tests/utils/invite.ts index 6712362a6c..a995476e90 100644 --- a/packages/backend/server/tests/utils/invite.ts +++ b/packages/backend/server/tests/utils/invite.ts @@ -112,7 +112,6 @@ export async function acceptInviteById( }) .expect(200); if (res.body.errors) { - console.error(res.body.errors); throw new Error(res.body.errors[0].message, { cause: res.body.errors[0].cause, });