diff --git a/packages/backend/server/src/base/error/def.ts b/packages/backend/server/src/base/error/def.ts
index 21b9e354f3..87b620f0b3 100644
--- a/packages/backend/server/src/base/error/def.ts
+++ b/packages/backend/server/src/base/error/def.ts
@@ -347,6 +347,11 @@ export const USER_FRIENDLY_ERRORS = {
args: { spaceId: 'string' },
message: ({ spaceId }) => `Space ${spaceId} not found.`,
},
+ member_not_found_in_space: {
+ type: 'action_forbidden',
+ args: { spaceId: 'string' },
+ message: ({ spaceId }) => `Member not found in Space ${spaceId}.`,
+ },
not_in_space: {
type: 'action_forbidden',
args: { spaceId: 'string' },
diff --git a/packages/backend/server/src/base/error/errors.gen.ts b/packages/backend/server/src/base/error/errors.gen.ts
index e87010b0be..01273cb870 100644
--- a/packages/backend/server/src/base/error/errors.gen.ts
+++ b/packages/backend/server/src/base/error/errors.gen.ts
@@ -191,6 +191,16 @@ export class SpaceNotFound extends UserFriendlyError {
}
}
@ObjectType()
+class MemberNotFoundInSpaceDataType {
+ @Field() spaceId!: string
+}
+
+export class MemberNotFoundInSpace extends UserFriendlyError {
+ constructor(args: MemberNotFoundInSpaceDataType, message?: string | ((args: MemberNotFoundInSpaceDataType) => string)) {
+ super('action_forbidden', 'member_not_found_in_space', message, args);
+ }
+}
+@ObjectType()
class NotInSpaceDataType {
@Field() spaceId!: string
}
@@ -615,6 +625,7 @@ export enum ErrorNames {
ACCESS_DENIED,
EMAIL_VERIFICATION_REQUIRED,
SPACE_NOT_FOUND,
+ MEMBER_NOT_FOUND_IN_SPACE,
NOT_IN_SPACE,
ALREADY_IN_SPACE,
SPACE_ACCESS_DENIED,
@@ -674,5 +685,5 @@ registerEnumType(ErrorNames, {
export const ErrorDataUnionType = createUnionType({
name: 'ErrorDataUnion',
types: () =>
- [WrongSignInCredentialsDataType, UnknownOauthProviderDataType, MissingOauthQueryParameterDataType, InvalidEmailDataType, InvalidPasswordLengthDataType, SpaceNotFoundDataType, NotInSpaceDataType, AlreadyInSpaceDataType, SpaceAccessDeniedDataType, SpaceOwnerNotFoundDataType, DocNotFoundDataType, DocAccessDeniedDataType, VersionRejectedDataType, InvalidHistoryTimestampDataType, DocHistoryNotFoundDataType, BlobNotFoundDataType, UnsupportedSubscriptionPlanDataType, SubscriptionAlreadyExistsDataType, SubscriptionNotExistsDataType, SameSubscriptionRecurringDataType, SubscriptionPlanNotFoundDataType, CopilotMessageNotFoundDataType, CopilotPromptNotFoundDataType, CopilotProviderSideErrorDataType, RuntimeConfigNotFoundDataType, InvalidRuntimeConfigTypeDataType] as const,
+ [WrongSignInCredentialsDataType, UnknownOauthProviderDataType, MissingOauthQueryParameterDataType, InvalidEmailDataType, InvalidPasswordLengthDataType, SpaceNotFoundDataType, MemberNotFoundInSpaceDataType, NotInSpaceDataType, AlreadyInSpaceDataType, SpaceAccessDeniedDataType, SpaceOwnerNotFoundDataType, DocNotFoundDataType, DocAccessDeniedDataType, VersionRejectedDataType, InvalidHistoryTimestampDataType, DocHistoryNotFoundDataType, BlobNotFoundDataType, UnsupportedSubscriptionPlanDataType, SubscriptionAlreadyExistsDataType, SubscriptionNotExistsDataType, SameSubscriptionRecurringDataType, SubscriptionPlanNotFoundDataType, CopilotMessageNotFoundDataType, CopilotPromptNotFoundDataType, CopilotProviderSideErrorDataType, RuntimeConfigNotFoundDataType, InvalidRuntimeConfigTypeDataType] as const,
});
diff --git a/packages/backend/server/src/core/workspaces/resolvers/team.ts b/packages/backend/server/src/core/workspaces/resolvers/team.ts
index 2d584ffddc..87897f8834 100644
--- a/packages/backend/server/src/core/workspaces/resolvers/team.ts
+++ b/packages/backend/server/src/core/workspaces/resolvers/team.ts
@@ -13,7 +13,7 @@ import {
Cache,
EventEmitter,
type EventPayload,
- NotInSpace,
+ MemberNotFoundInSpace,
OnEvent,
RequestMutex,
TooManyRequest,
@@ -152,7 +152,16 @@ export class TeamWorkspaceResolver {
description: 'invite link for workspace',
nullable: true,
})
- async inviteLink(@Parent() workspace: WorkspaceType) {
+ async inviteLink(
+ @Parent() workspace: WorkspaceType,
+ @CurrentUser() user: CurrentUser
+ ) {
+ await this.permissions.checkWorkspace(
+ workspace.id,
+ user.id,
+ Permission.Admin
+ );
+
const cacheId = `workspace:inviteLink:${workspace.id}`;
const id = await this.cache.get<{ inviteId: string }>(cacheId);
if (id) {
@@ -261,7 +270,7 @@ export class TeamWorkspaceResolver {
}
return new TooManyRequest();
} else {
- return new NotInSpace({ spaceId: workspaceId });
+ return new MemberNotFoundInSpace({ spaceId: workspaceId });
}
} catch (e) {
this.logger.error('failed to invite user', e);
@@ -307,7 +316,7 @@ export class TeamWorkspaceResolver {
return result;
} else {
- return new NotInSpace({ spaceId: workspaceId });
+ return new MemberNotFoundInSpace({ spaceId: workspaceId });
}
} catch (e) {
this.logger.error('failed to invite user', e);
diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql
index 19763fda7e..aa59d72efc 100644
--- a/packages/backend/server/src/schema.gql
+++ b/packages/backend/server/src/schema.gql
@@ -209,7 +209,7 @@ type EditorType {
name: String!
}
-union ErrorDataUnion = AlreadyInSpaceDataType | BlobNotFoundDataType | CopilotMessageNotFoundDataType | CopilotPromptNotFoundDataType | CopilotProviderSideErrorDataType | DocAccessDeniedDataType | DocHistoryNotFoundDataType | DocNotFoundDataType | InvalidEmailDataType | InvalidHistoryTimestampDataType | InvalidPasswordLengthDataType | InvalidRuntimeConfigTypeDataType | MissingOauthQueryParameterDataType | NotInSpaceDataType | RuntimeConfigNotFoundDataType | SameSubscriptionRecurringDataType | SpaceAccessDeniedDataType | SpaceNotFoundDataType | SpaceOwnerNotFoundDataType | SubscriptionAlreadyExistsDataType | SubscriptionNotExistsDataType | SubscriptionPlanNotFoundDataType | UnknownOauthProviderDataType | UnsupportedSubscriptionPlanDataType | VersionRejectedDataType | WrongSignInCredentialsDataType
+union ErrorDataUnion = AlreadyInSpaceDataType | BlobNotFoundDataType | CopilotMessageNotFoundDataType | CopilotPromptNotFoundDataType | CopilotProviderSideErrorDataType | DocAccessDeniedDataType | DocHistoryNotFoundDataType | DocNotFoundDataType | InvalidEmailDataType | InvalidHistoryTimestampDataType | InvalidPasswordLengthDataType | InvalidRuntimeConfigTypeDataType | MemberNotFoundInSpaceDataType | MissingOauthQueryParameterDataType | NotInSpaceDataType | RuntimeConfigNotFoundDataType | SameSubscriptionRecurringDataType | SpaceAccessDeniedDataType | SpaceNotFoundDataType | SpaceOwnerNotFoundDataType | SubscriptionAlreadyExistsDataType | SubscriptionNotExistsDataType | SubscriptionPlanNotFoundDataType | UnknownOauthProviderDataType | UnsupportedSubscriptionPlanDataType | VersionRejectedDataType | WrongSignInCredentialsDataType
enum ErrorNames {
ACCESS_DENIED
@@ -257,6 +257,7 @@ enum ErrorNames {
INVALID_SUBSCRIPTION_PARAMETERS
LINK_EXPIRED
MAILER_SERVICE_IS_NOT_CONFIGURED
+ MEMBER_NOT_FOUND_IN_SPACE
MEMBER_QUOTA_EXCEEDED
MISSING_OAUTH_QUERY_PARAMETER
NOT_FOUND
@@ -472,6 +473,10 @@ input ManageUserInput {
name: String
}
+type MemberNotFoundInSpaceDataType {
+ spaceId: String!
+}
+
type MissingOauthQueryParameterDataType {
name: String!
}
diff --git a/packages/backend/server/tests/team.e2e.ts b/packages/backend/server/tests/team.e2e.ts
index e48d3f96f4..4be4b0c0a2 100644
--- a/packages/backend/server/tests/team.e2e.ts
+++ b/packages/backend/server/tests/team.e2e.ts
@@ -1,11 +1,16 @@
///
+import { randomUUID } from 'node:crypto';
+
+import { getCurrentMailMessageCount } from '@affine-test/kit/utils/cloud';
import { INestApplication } from '@nestjs/common';
import { WorkspaceMemberStatus } from '@prisma/client';
import type { TestFn } from 'ava';
import ava from 'ava';
+import Sinon from 'sinon';
import { AppModule } from '../src/app.module';
+import { EventEmitter } from '../src/base';
import { AuthService } from '../src/core/auth';
import { DocContentService } from '../src/core/doc-renderer';
import { Permission, PermissionService } from '../src/core/permission';
@@ -17,15 +22,20 @@ import {
import { WorkspaceType } from '../src/core/workspaces';
import {
acceptInviteById,
+ approveMember,
createInviteLink,
createTestingApp,
createWorkspace,
getInviteInfo,
+ getInviteLink,
+ getWorkspace,
grantMember,
inviteUser,
inviteUsers,
leaveWorkspace,
PermissionEnum,
+ revokeInviteLink,
+ revokeUser,
signUp,
sleep,
UserAuthedType,
@@ -34,6 +44,7 @@ import {
const test = ava as TestFn<{
app: INestApplication;
auth: AuthService;
+ event: Sinon.SinonStubbedInstance;
quota: QuotaService;
quotaManager: QuotaManagementService;
permissions: PermissionService;
@@ -43,6 +54,9 @@ test.beforeEach(async t => {
const { app } = await createTestingApp({
imports: [AppModule],
tapModule: module => {
+ module
+ .overrideProvider(EventEmitter)
+ .useValue(Sinon.createStubInstance(EventEmitter));
module.overrideProvider(DocContentService).useValue({
getWorkspaceContent() {
return {
@@ -54,50 +68,84 @@ test.beforeEach(async t => {
},
});
- const quota = app.get(QuotaService);
- const quotaManager = app.get(QuotaManagementService);
- const permissions = app.get(PermissionService);
- const auth = app.get(AuthService);
-
t.context.app = app;
- t.context.quota = quota;
- t.context.quotaManager = quotaManager;
- t.context.permissions = permissions;
- t.context.auth = auth;
+ t.context.auth = app.get(AuthService);
+ t.context.event = app.get(EventEmitter);
+ t.context.quota = app.get(QuotaService);
+ t.context.quotaManager = app.get(QuotaManagementService);
+ t.context.permissions = app.get(PermissionService);
});
test.afterEach.always(async t => {
await t.context.app.close();
});
-const init = async (app: INestApplication, memberLimit = 10) => {
- const owner = await signUp(app, 'test', 'test@affine.pro', '123456');
- const workspace = await createWorkspace(app, owner.token.token);
+const init = async (
+ app: INestApplication,
+ memberLimit = 10,
+ prefix = randomUUID()
+) => {
+ const owner = await signUp(
+ app,
+ 'owner',
+ `${prefix}owner@affine.pro`,
+ '123456'
+ );
+ {
+ const quota = app.get(QuotaService);
+ await quota.switchUserQuota(owner.id, QuotaType.ProPlanV1);
+ }
+ const workspace = await createWorkspace(app, owner.token.token);
const teamWorkspace = await createWorkspace(app, owner.token.token);
- const quota = app.get(QuotaManagementService);
- await quota.addTeamWorkspace(teamWorkspace.id, 'test');
- await quota.updateWorkspaceConfig(teamWorkspace.id, QuotaType.TeamPlanV1, {
- memberLimit,
- });
+ {
+ const quota = app.get(QuotaManagementService);
+ await quota.addTeamWorkspace(teamWorkspace.id, 'test');
+ await quota.updateWorkspaceConfig(teamWorkspace.id, QuotaType.TeamPlanV1, {
+ memberLimit,
+ });
+ }
const invite = async (
email: string,
- permission: PermissionEnum = 'Write'
+ permission: PermissionEnum = 'Write',
+ shouldSendEmail: boolean = false
) => {
const member = await signUp(app, email.split('@')[0], email, '123456');
- const inviteId = await inviteUser(
- app,
- owner.token.token,
- teamWorkspace.id,
- member.email,
- permission
- );
- await acceptInviteById(app, teamWorkspace.id, inviteId);
+
+ {
+ // normal workspace
+ const inviteId = await inviteUser(
+ app,
+ owner.token.token,
+ workspace.id,
+ member.email,
+ permission,
+ shouldSendEmail
+ );
+ await acceptInviteById(app, workspace.id, inviteId, shouldSendEmail);
+ }
+
+ {
+ // team workspace
+ const inviteId = await inviteUser(
+ app,
+ owner.token.token,
+ teamWorkspace.id,
+ member.email,
+ permission,
+ shouldSendEmail
+ );
+ await acceptInviteById(app, teamWorkspace.id, inviteId, shouldSendEmail);
+ }
+
return member;
};
- const inviteBatch = async (emails: string[]) => {
+ const inviteBatch = async (
+ emails: string[],
+ shouldSendEmail: boolean = false
+ ) => {
const members = [];
for (const email of emails) {
const member = await signUp(app, email.split('@')[0], email, '123456');
@@ -107,7 +155,8 @@ const init = async (app: INestApplication, memberLimit = 10) => {
app,
owner.token.token,
teamWorkspace.id,
- emails
+ emails,
+ shouldSendEmail
);
return [members, invites] as const;
};
@@ -122,9 +171,18 @@ const init = async (app: INestApplication, memberLimit = 10) => {
const inviteId = link.split('/').pop()!;
return [
inviteId,
- async (email: string): Promise => {
+ async (
+ email: string,
+ shouldSendEmail: boolean = false
+ ): Promise => {
const member = await signUp(app, email.split('@')[0], email, '123456');
- await acceptInviteById(app, ws.id, inviteId, false, member.token.token);
+ await acceptInviteById(
+ app,
+ ws.id,
+ inviteId,
+ shouldSendEmail,
+ member.token.token
+ );
return member;
},
async (token: string) => {
@@ -133,9 +191,9 @@ const init = async (app: INestApplication, memberLimit = 10) => {
] as const;
};
- const admin = await invite('admin@affine.pro', 'Admin');
- const write = await invite('member1@affine.pro');
- const read = await invite('member2@affine.pro', 'Read');
+ const admin = await invite(`${prefix}admin@affine.pro`, 'Admin');
+ const write = await invite(`${prefix}write@affine.pro`);
+ const read = await invite(`${prefix}read@affine.pro`, 'Read');
return {
invite,
@@ -150,6 +208,57 @@ const init = async (app: INestApplication, memberLimit = 10) => {
};
};
+test('should be able to invite multiple users', async t => {
+ const { app } = t.context;
+ const { teamWorkspace: ws, owner, admin, write, read } = await init(app, 4);
+
+ {
+ // no permission
+ await t.throwsAsync(
+ inviteUsers(app, read.token.token, ws.id, ['test@affine.pro']),
+ { instanceOf: Error },
+ 'should throw error if not manager'
+ );
+ await t.throwsAsync(
+ inviteUsers(app, write.token.token, ws.id, ['test@affine.pro']),
+ { instanceOf: Error },
+ 'should throw error if not manager'
+ );
+ }
+
+ {
+ // manager
+ const m1 = await signUp(app, 'm1', 'm1@affine.pro', '123456');
+ const m2 = await signUp(app, 'm2', 'm2@affine.pro', '123456');
+ t.is(
+ (await inviteUsers(app, owner.token.token, ws.id, [m1.email])).length,
+ 1,
+ 'should be able to invite user'
+ );
+ t.is(
+ (await inviteUsers(app, admin.token.token, ws.id, [m2.email])).length,
+ 1,
+ 'should be able to invite user'
+ );
+ t.is(
+ (await inviteUsers(app, admin.token.token, ws.id, [m2.email])).length,
+ 0,
+ 'should not be able to invite user if already in workspace'
+ );
+
+ await t.throwsAsync(
+ inviteUsers(
+ app,
+ admin.token.token,
+ ws.id,
+ Array.from({ length: 513 }, (_, i) => `m${i}@affine.pro`)
+ ),
+ { message: 'Too many requests.' },
+ 'should throw error if exceed maximum number of invitations per request'
+ );
+ }
+});
+
test('should be able to check seat limit', async t => {
const { app, permissions, quotaManager } = t.context;
const { invite, inviteBatch, teamWorkspace: ws } = await init(app, 4);
@@ -267,6 +376,161 @@ test('should be able to leave workspace', async t => {
);
});
+test('should be able to revoke team member', async t => {
+ const { app } = t.context;
+ const { teamWorkspace: ws, owner, admin, write, read } = await init(app);
+
+ {
+ // no permission
+ t.throwsAsync(
+ revokeUser(app, read.token.token, ws.id, read.id),
+ { instanceOf: Error },
+ 'should throw error if not admin'
+ );
+ t.throwsAsync(
+ revokeUser(app, read.token.token, ws.id, write.id),
+ { instanceOf: Error },
+ 'should throw error if not admin'
+ );
+ }
+
+ {
+ // manager
+ t.true(
+ await revokeUser(app, admin.token.token, ws.id, read.id),
+ 'admin should be able to revoke member'
+ );
+
+ t.true(
+ await revokeUser(app, owner.token.token, ws.id, write.id),
+ 'owner should be able to revoke member'
+ );
+
+ await t.throwsAsync(
+ revokeUser(app, admin.token.token, ws.id, admin.id),
+ { instanceOf: Error },
+ 'should not be able to revoke themselves'
+ );
+
+ t.false(
+ await revokeUser(app, owner.token.token, ws.id, owner.id),
+ 'should not be able to revoke themselves'
+ );
+
+ await revokeUser(app, owner.token.token, ws.id, admin.id);
+ await t.throwsAsync(
+ revokeUser(app, admin.token.token, ws.id, read.id),
+ { instanceOf: Error },
+ 'should not be able to revoke member not in workspace'
+ );
+ }
+});
+
+test('should be able to manage invite link', async t => {
+ const { app } = t.context;
+ const {
+ workspace: ws,
+ teamWorkspace: tws,
+ owner,
+ admin,
+ write,
+ read,
+ } = await init(app, 4);
+
+ for (const workspace of [ws, tws]) {
+ for (const manager of [owner, admin]) {
+ const { link } = await createInviteLink(
+ app,
+ manager.token.token,
+ workspace.id,
+ 'OneDay'
+ );
+ const { link: currLink } = await getInviteLink(
+ app,
+ manager.token.token,
+ workspace.id
+ );
+ t.is(link, currLink, 'should be able to get invite link');
+
+ t.true(
+ await revokeInviteLink(app, manager.token.token, workspace.id),
+ 'should be able to revoke invite link'
+ );
+ }
+
+ for (const collaborator of [write, read]) {
+ await t.throwsAsync(
+ createInviteLink(app, collaborator.token.token, workspace.id, 'OneDay'),
+ { instanceOf: Error },
+ 'should throw error if not manager'
+ );
+ await t.throwsAsync(
+ getInviteLink(app, collaborator.token.token, workspace.id),
+ { instanceOf: Error },
+ 'should throw error if not manager'
+ );
+ await t.throwsAsync(
+ revokeInviteLink(app, collaborator.token.token, workspace.id),
+ { instanceOf: Error },
+ 'should throw error if not manager'
+ );
+ }
+ }
+});
+
+test('should be able to approve team member', async t => {
+ const { app } = t.context;
+ const { teamWorkspace: tws, owner, admin, write, read } = await init(app, 5);
+
+ {
+ const { link } = await createInviteLink(
+ app,
+ owner.token.token,
+ tws.id,
+ 'OneDay'
+ );
+ const inviteId = link.split('/').pop()!;
+
+ const member = await signUp(
+ app,
+ 'newmember',
+ 'newmember@affine.pro',
+ '123456'
+ );
+ t.true(
+ await acceptInviteById(app, tws.id, inviteId, false, member.token.token),
+ 'should be able to accept invite'
+ );
+
+ const { members } = await getWorkspace(app, owner.token.token, tws.id);
+ const memberInvite = members.find(m => m.id === member.id)!;
+ t.is(memberInvite.status, 'UnderReview', 'should be under review');
+
+ t.is(
+ await approveMember(app, admin.token.token, tws.id, member.id),
+ memberInvite.inviteId
+ );
+ }
+
+ {
+ await t.throwsAsync(
+ approveMember(app, admin.token.token, tws.id, 'not_exists_id'),
+ { instanceOf: Error },
+ 'should throw error if member not exists'
+ );
+ await t.throwsAsync(
+ approveMember(app, write.token.token, tws.id, 'not_exists_id'),
+ { instanceOf: Error },
+ 'should throw error if not manager'
+ );
+ await t.throwsAsync(
+ approveMember(app, read.token.token, tws.id, 'not_exists_id'),
+ { instanceOf: Error },
+ 'should throw error if not manager'
+ );
+ }
+});
+
test('should be able to invite by link', async t => {
const { app, permissions, quotaManager } = t.context;
const {
@@ -291,19 +555,21 @@ test('should be able to invite by link', async t => {
{
// invite link
- const t1 = await invite('test1@affine.pro');
- const t2 = await invite('test2@affine.pro');
+ for (const [i] of Array.from({ length: 6 }).entries()) {
+ const user = await invite(`test${i}@affine.pro`);
+ const status = await permissions.getWorkspaceMemberStatus(ws.id, user.id);
+ t.is(
+ status,
+ WorkspaceMemberStatus.Accepted,
+ 'should be able to check status'
+ );
+ }
await t.throwsAsync(
- invite('test3@affine.pro'),
+ invite('exceed@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');
}
{
@@ -361,3 +627,56 @@ test('should be able to invite by link', async t => {
}
}
});
+
+test('should be able to send mails', async t => {
+ const { app } = t.context;
+ const { inviteBatch } = await init(app, 4);
+ const primitiveMailCount = await getCurrentMailMessageCount();
+
+ {
+ await inviteBatch(['m3@affine.pro', 'm4@affine.pro'], true);
+ t.is(await getCurrentMailMessageCount(), primitiveMailCount + 2);
+ }
+});
+
+test('should be able to emit events', async t => {
+ const { app, event } = t.context;
+
+ {
+ const { teamWorkspace: tws, inviteBatch } = await init(app, 4);
+
+ await inviteBatch(['m1@affine.pro', 'm2@affine.pro']);
+ t.true(
+ event.emit.calledOnceWith('workspace.members.updated', {
+ workspaceId: tws.id,
+ count: 6,
+ })
+ );
+ }
+
+ {
+ const { teamWorkspace: tws, owner, createInviteLink } = await init(app, 10);
+ const [, invite] = await createInviteLink(tws);
+ const user = await invite('m3@affine.pro');
+ const { members } = await getWorkspace(app, owner.token.token, tws.id);
+ const memberInvite = members.find(m => m.id === user.id)!;
+ t.deepEqual(
+ event.emit.lastCall.args,
+ [
+ 'workspace.members.reviewRequested',
+ { inviteId: memberInvite.inviteId },
+ ],
+ 'should emit review requested event'
+ );
+
+ await revokeUser(app, owner.token.token, tws.id, user.id);
+ t.deepEqual(
+ event.emit.lastCall.args,
+ [
+ 'workspace.members.requestDeclined',
+ { inviteId: memberInvite.inviteId },
+ ],
+ 'should emit review requested event'
+ );
+ }
+});
diff --git a/packages/backend/server/tests/utils/invite.ts b/packages/backend/server/tests/utils/invite.ts
index a995476e90..218ddad371 100644
--- a/packages/backend/server/tests/utils/invite.ts
+++ b/packages/backend/server/tests/utils/invite.ts
@@ -65,6 +65,34 @@ export async function inviteUsers(
return res.body.data.inviteBatch;
}
+export async function getInviteLink(
+ app: INestApplication,
+ token: string,
+ workspaceId: string
+): Promise<{ link: string; expireTime: string }> {
+ const res = await request(app.getHttpServer())
+ .post(gql)
+ .auth(token, { type: 'bearer' })
+ .set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
+ .send({
+ query: `
+ query {
+ workspace(id: "${workspaceId}") {
+ inviteLink {
+ link
+ expireTime
+ }
+ }
+ }
+ `,
+ })
+ .expect(200);
+ if (res.body.errors) {
+ throw new Error(res.body.errors[0].message);
+ }
+ return res.body.data.workspace.inviteLink;
+}
+
export async function createInviteLink(
app: INestApplication,
token: string,
@@ -92,6 +120,29 @@ export async function createInviteLink(
return res.body.data.createInviteLink;
}
+export async function revokeInviteLink(
+ app: INestApplication,
+ token: string,
+ workspaceId: string
+): Promise {
+ const res = await request(app.getHttpServer())
+ .post(gql)
+ .auth(token, { type: 'bearer' })
+ .set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
+ .send({
+ query: `
+ mutation {
+ revokeInviteLink(workspaceId: "${workspaceId}")
+ }
+ `,
+ })
+ .expect(200);
+ if (res.body.errors) {
+ throw new Error(res.body.errors[0].message);
+ }
+ return res.body.data.revokeInviteLink;
+}
+
export async function acceptInviteById(
app: INestApplication,
workspaceId: string,
@@ -119,6 +170,32 @@ export async function acceptInviteById(
return res.body.data.acceptInviteById;
}
+export async function approveMember(
+ app: INestApplication,
+ token: string,
+ workspaceId: string,
+ userId: string
+): Promise {
+ const res = await request(app.getHttpServer())
+ .post(gql)
+ .set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
+ .auth(token, { type: 'bearer' })
+ .send({
+ query: `
+ mutation {
+ approveMember(workspaceId: "${workspaceId}", userId: "${userId}")
+ }
+ `,
+ })
+ .expect(200);
+ if (res.body.errors) {
+ throw new Error(res.body.errors[0].message, {
+ cause: res.body.errors[0].cause,
+ });
+ }
+ return res.body.data.approveMember;
+}
+
export async function leaveWorkspace(
app: INestApplication,
token: string,
@@ -161,6 +238,11 @@ export async function revokeUser(
`,
})
.expect(200);
+ if (res.body.errors) {
+ throw new Error(res.body.errors[0].message, {
+ cause: res.body.errors[0].cause,
+ });
+ }
return res.body.data.revoke;
}
diff --git a/packages/backend/server/tests/utils/workspace.ts b/packages/backend/server/tests/utils/workspace.ts
index a2cf6c2716..8fcdde1553 100644
--- a/packages/backend/server/tests/utils/workspace.ts
+++ b/packages/backend/server/tests/utils/workspace.ts
@@ -71,7 +71,7 @@ export async function getWorkspace(
query: `
query {
workspace(id: "${workspaceId}") {
- id, members(skip: ${skip}, take: ${take}) { id, name, email, permission, inviteId }
+ id, members(skip: ${skip}, take: ${take}) { id, name, email, permission, inviteId, status }
}
}
`,
diff --git a/packages/frontend/graphql/src/schema.ts b/packages/frontend/graphql/src/schema.ts
index 4baeb351ad..7085df1e23 100644
--- a/packages/frontend/graphql/src/schema.ts
+++ b/packages/frontend/graphql/src/schema.ts
@@ -273,6 +273,7 @@ export type ErrorDataUnion =
| InvalidHistoryTimestampDataType
| InvalidPasswordLengthDataType
| InvalidRuntimeConfigTypeDataType
+ | MemberNotFoundInSpaceDataType
| MissingOauthQueryParameterDataType
| NotInSpaceDataType
| RuntimeConfigNotFoundDataType
@@ -334,6 +335,7 @@ export enum ErrorNames {
INVALID_SUBSCRIPTION_PARAMETERS = 'INVALID_SUBSCRIPTION_PARAMETERS',
LINK_EXPIRED = 'LINK_EXPIRED',
MAILER_SERVICE_IS_NOT_CONFIGURED = 'MAILER_SERVICE_IS_NOT_CONFIGURED',
+ MEMBER_NOT_FOUND_IN_SPACE = 'MEMBER_NOT_FOUND_IN_SPACE',
MEMBER_QUOTA_EXCEEDED = 'MEMBER_QUOTA_EXCEEDED',
MISSING_OAUTH_QUERY_PARAMETER = 'MISSING_OAUTH_QUERY_PARAMETER',
NOT_FOUND = 'NOT_FOUND',
@@ -541,6 +543,11 @@ export interface ManageUserInput {
name?: InputMaybe;
}
+export interface MemberNotFoundInSpaceDataType {
+ __typename?: 'MemberNotFoundInSpaceDataType';
+ spaceId: Scalars['String']['output'];
+}
+
export interface MissingOauthQueryParameterDataType {
__typename?: 'MissingOauthQueryParameterDataType';
name: Scalars['String']['output'];