feat(server): role changed email (#9227)

This commit is contained in:
darkskygit
2024-12-23 10:13:04 +00:00
parent 067469aa98
commit aacdb71ee2
21 changed files with 238 additions and 155 deletions
@@ -166,7 +166,7 @@ function generateErrorArgs(name: string, args: ErrorArgs) {
export function generateUserFriendlyErrors() { export function generateUserFriendlyErrors() {
const output = [ const output = [
'/* eslint-disable */', '/* oxlint-disable */',
'// AUTO GENERATED FILE', '// AUTO GENERATED FILE',
`import { createUnionType, Field, ObjectType, registerEnumType } from '@nestjs/graphql';`, `import { createUnionType, Field, ObjectType, registerEnumType } from '@nestjs/graphql';`,
'', '',
@@ -374,10 +374,6 @@ export const USER_FRIENDLY_ERRORS = {
args: { spaceId: 'string' }, args: { spaceId: 'string' },
message: ({ spaceId }) => `Owner of Space ${spaceId} not found.`, message: ({ spaceId }) => `Owner of Space ${spaceId} not found.`,
}, },
cant_change_space_owner: {
type: 'action_forbidden',
message: 'You are not allowed to change the owner of a Space.',
},
doc_not_found: { doc_not_found: {
type: 'resource_not_found', type: 'resource_not_found',
args: { spaceId: 'string', docId: 'string' }, args: { spaceId: 'string', docId: 'string' },
@@ -1,4 +1,4 @@
/* eslint-disable */ /* oxlint-disable */
// AUTO GENERATED FILE // AUTO GENERATED FILE
import { createUnionType, Field, ObjectType, registerEnumType } from '@nestjs/graphql'; import { createUnionType, Field, ObjectType, registerEnumType } from '@nestjs/graphql';
@@ -240,12 +240,6 @@ export class SpaceOwnerNotFound extends UserFriendlyError {
super('internal_server_error', 'space_owner_not_found', message, args); super('internal_server_error', 'space_owner_not_found', message, args);
} }
} }
export class CantChangeSpaceOwner extends UserFriendlyError {
constructor(message?: string) {
super('action_forbidden', 'cant_change_space_owner', message);
}
}
@ObjectType() @ObjectType()
class DocNotFoundDataType { class DocNotFoundDataType {
@Field() spaceId!: string @Field() spaceId!: string
@@ -630,7 +624,6 @@ export enum ErrorNames {
ALREADY_IN_SPACE, ALREADY_IN_SPACE,
SPACE_ACCESS_DENIED, SPACE_ACCESS_DENIED,
SPACE_OWNER_NOT_FOUND, SPACE_OWNER_NOT_FOUND,
CANT_CHANGE_SPACE_OWNER,
DOC_NOT_FOUND, DOC_NOT_FOUND,
DOC_ACCESS_DENIED, DOC_ACCESS_DENIED,
VERSION_REJECTED, VERSION_REJECTED,
@@ -10,6 +10,12 @@ export interface WorkspaceEvents {
workspaceId: Workspace['id']; workspaceId: Workspace['id'];
}>; }>;
requestApproved: Payload<{ inviteId: string }>; requestApproved: Payload<{ inviteId: string }>;
roleChanged: Payload<{
userId: User['id'];
workspaceId: Workspace['id'];
permission: number;
}>;
ownerTransferred: Payload<{ email: string; workspaceId: Workspace['id'] }>;
updated: Payload<{ workspaceId: Workspace['id']; count: number }>; updated: Payload<{ workspaceId: Workspace['id']; count: number }>;
}; };
deleted: Payload<Workspace['id']>; deleted: Payload<Workspace['id']>;
@@ -6,7 +6,12 @@ import { URLHelper } from '../helpers';
import { metrics } from '../metrics'; import { metrics } from '../metrics';
import type { MailerService, Options } from './mailer'; import type { MailerService, Options } from './mailer';
import { MAILER_SERVICE } from './mailer'; import { MAILER_SERVICE } from './mailer';
import { emailTemplate } from './template'; import {
emailTemplate,
getRoleChangedTemplate,
type RoleChangedMailParams,
} from './template';
@Injectable() @Injectable()
export class MailService { export class MailService {
constructor( constructor(
@@ -311,4 +316,22 @@ export class MailService {
}); });
return this.sendMail({ to, subject: title, html }); return this.sendMail({ to, subject: title, html });
} }
async sendRoleChangedEmail(to: string, ws: RoleChangedMailParams) {
const { subject, title, content } = getRoleChangedTemplate(ws);
const html = emailTemplate({ title, content });
console.log({ subject, title, content, to });
return this.sendMail({ to, subject, html });
}
async sendOwnerTransferred(to: string, ws: { name: string }) {
const { name: workspaceName } = ws;
const title = `Your ownership of ${workspaceName} has been transferred`;
const html = emailTemplate({
title: 'Ownership transferred',
content: `You have transferred ownership of ${workspaceName}. You are now a admin in this workspace.`,
});
return this.sendMail({ to, subject: title, html });
}
} }
@@ -219,3 +219,38 @@ export const emailTemplate = ({
</table> </table>
</body>`; </body>`;
}; };
type RoleChangedMail = {
subject: string;
title: string;
content: string;
};
export type RoleChangedMailParams = {
name: string;
role: 'owner' | 'admin' | 'member' | 'readonly';
};
export const getRoleChangedTemplate = (
ws: RoleChangedMailParams
): RoleChangedMail => {
const { name, role } = ws;
let subject = `You are now an ${role} of ${name}`;
let title = 'Role update in workspace';
let content = `Your role in ${name} has been changed to ${role}. You can continue to collaborate in this workspace.`;
switch (role) {
case 'owner':
title = 'Welcome, new workspace owner!';
content = `You have been assigned as the owner of ${name}. As a workspace owner, you have full control over this team workspace.`;
break;
case 'admin':
title = `You've been promoted to admin.`;
content = `You have been promoted to admin of ${name}. As an admin, you can help the workspace owner manage members in this workspace.`;
break;
default:
subject = `Your role has been changed in ${name}`;
break;
}
return { subject, title, content };
};
@@ -322,10 +322,6 @@ export class PermissionService {
this.prisma.workspaceUserPermission.update({ this.prisma.workspaceUserPermission.update({
where: { where: {
workspaceId_userId: { workspaceId: ws, userId: user }, workspaceId_userId: { workspaceId: ws, userId: user },
// only update permission:
// 1. if the new permission is owner and original permission is admin
// 2. if the original permission is not owner
type: toBeOwner ? Permission.Admin : { not: Permission.Owner },
}, },
data: { type: permission }, data: { type: permission },
}), }),
@@ -2,9 +2,9 @@ import { Injectable, Logger } from '@nestjs/common';
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
import { getStreamAsBuffer } from 'get-stream'; import { getStreamAsBuffer } from 'get-stream';
import { Cache, MailService } from '../../../base'; import { Cache, MailService, UserNotFound } from '../../../base';
import { DocContentService } from '../../doc-renderer'; import { DocContentService } from '../../doc-renderer';
import { PermissionService } from '../../permission'; import { Permission, PermissionService } from '../../permission';
import { WorkspaceBlobStorage } from '../../storage'; import { WorkspaceBlobStorage } from '../../storage';
import { UserService } from '../../user'; import { UserService } from '../../user';
@@ -17,6 +17,13 @@ export type InviteInfo = {
inviteeUserId?: string; inviteeUserId?: string;
}; };
const PermissionToRole = {
[Permission.Read]: 'readonly' as const,
[Permission.Write]: 'member' as const,
[Permission.Admin]: 'admin' as const,
[Permission.Owner]: 'owner' as const,
};
@Injectable() @Injectable()
export class WorkspaceService { export class WorkspaceService {
private readonly logger = new Logger(WorkspaceService.name); private readonly logger = new Logger(WorkspaceService.name);
@@ -78,6 +85,27 @@ export class WorkspaceService {
}; };
} }
private async getInviteeEmailTarget(inviteId: string) {
const { workspaceId, inviteeUserId } = await this.getInviteInfo(inviteId);
if (!inviteeUserId) {
this.logger.error(`Invitee user not found for inviteId: ${inviteId}`);
return;
}
const workspace = await this.getWorkspaceInfo(workspaceId);
const invitee = await this.user.findUserById(inviteeUserId);
if (!invitee) {
this.logger.error(
`Invitee user not found in workspace: ${workspaceId}, userId: ${inviteeUserId}`
);
return;
}
return {
email: invitee.email,
workspace,
};
}
async sendAcceptedEmail(inviteId: string) { async sendAcceptedEmail(inviteId: string) {
const { workspaceId, inviterUserId, inviteeUserId } = const { workspaceId, inviterUserId, inviteeUserId } =
await this.getInviteInfo(inviteId); await this.getInviteInfo(inviteId);
@@ -167,24 +195,21 @@ export class WorkspaceService {
await this.mailer.sendReviewDeclinedEmail(email, { name: workspaceName }); await this.mailer.sendReviewDeclinedEmail(email, { name: workspaceName });
} }
private async getInviteeEmailTarget(inviteId: string) { async sendRoleChangedEmail(
const { workspaceId, inviteeUserId } = await this.getInviteInfo(inviteId); userId: string,
if (!inviteeUserId) { ws: { id: string; role: Permission }
this.logger.error(`Invitee user not found for inviteId: ${inviteId}`); ) {
return; const user = await this.user.findUserById(userId);
} if (!user) throw new UserNotFound();
const workspace = await this.getWorkspaceInfo(workspaceId); const workspace = await this.getWorkspaceInfo(ws.id);
const invitee = await this.user.findUserById(inviteeUserId); await this.mailer.sendRoleChangedEmail(user?.email, {
if (!invitee) { name: workspace.name,
this.logger.error( role: PermissionToRole[ws.role],
`Invitee user not found in workspace: ${workspaceId}, userId: ${inviteeUserId}` });
); }
return;
}
return { async sendOwnerTransferred(email: string, ws: { id: string }) {
email: invitee.email, const workspace = await this.getWorkspaceInfo(ws.id);
workspace, await this.mailer.sendOwnerTransferred(email, { name: workspace.name });
};
} }
} }
@@ -18,6 +18,7 @@ import {
RequestMutex, RequestMutex,
TooManyRequest, TooManyRequest,
URLHelper, URLHelper,
UserFriendlyError,
} from '../../../base'; } from '../../../base';
import { CurrentUser } from '../../auth'; import { CurrentUser } from '../../auth';
import { Permission, PermissionService } from '../../permission'; import { Permission, PermissionService } from '../../permission';
@@ -311,7 +312,17 @@ export class TeamWorkspaceResolver {
); );
if (result) { if (result) {
// TODO(@darkskygit): send team role changed mail this.event.emit('workspace.members.roleChanged', {
userId,
workspaceId,
permission,
});
if (permission === Permission.Owner) {
this.event.emit('workspace.members.ownerTransferred', {
email: user.email,
workspaceId,
});
}
} }
return result; return result;
@@ -320,6 +331,10 @@ export class TeamWorkspaceResolver {
} }
} catch (e) { } catch (e) {
this.logger.error('failed to invite user', e); this.logger.error('failed to invite user', e);
// pass through user friendly error
if (e instanceof UserFriendlyError) {
return e;
}
return new TooManyRequest(); return new TooManyRequest();
} }
} }
@@ -353,4 +368,28 @@ export class TeamWorkspaceResolver {
// send approve mail // send approve mail
await this.workspaceService.sendReviewApproveEmail(inviteId); await this.workspaceService.sendReviewApproveEmail(inviteId);
} }
@OnEvent('workspace.members.roleChanged')
async onRoleChanged({
userId,
workspaceId,
permission,
}: EventPayload<'workspace.members.roleChanged'>) {
// send role changed mail
await this.workspaceService.sendRoleChangedEmail(userId, {
id: workspaceId,
role: permission,
});
}
@OnEvent('workspace.members.ownerTransferred')
async onOwnerTransferred({
email,
workspaceId,
}: EventPayload<'workspace.members.ownerTransferred'>) {
// send role changed mail
await this.workspaceService.sendOwnerTransferred(email, {
id: workspaceId,
});
}
} }
@@ -17,7 +17,6 @@ import type { FileUpload } from '../../../base';
import { import {
AlreadyInSpace, AlreadyInSpace,
Cache, Cache,
CantChangeSpaceOwner,
DocNotFound, DocNotFound,
EventEmitter, EventEmitter,
InternalServerError, InternalServerError,
@@ -383,8 +382,13 @@ export class WorkspaceResolver {
@CurrentUser() user: CurrentUser, @CurrentUser() user: CurrentUser,
@Args('workspaceId') workspaceId: string, @Args('workspaceId') workspaceId: string,
@Args('email') email: string, @Args('email') email: string,
@Args('permission', { type: () => Permission }) permission: Permission, @Args('sendInviteMail', { nullable: true }) sendInviteMail: boolean,
@Args('sendInviteMail', { nullable: true }) sendInviteMail: boolean @Args('permission', {
type: () => Permission,
nullable: true,
deprecationReason: 'never used',
})
_permission?: Permission
) { ) {
await this.permissions.checkWorkspace( await this.permissions.checkWorkspace(
workspaceId, workspaceId,
@@ -392,10 +396,6 @@ export class WorkspaceResolver {
Permission.Admin Permission.Admin
); );
if (permission === Permission.Owner) {
throw new CantChangeSpaceOwner();
}
try { try {
// lock to prevent concurrent invite and grant // lock to prevent concurrent invite and grant
const lockFlag = `invite:${workspaceId}`; const lockFlag = `invite:${workspaceId}`;
@@ -428,7 +428,7 @@ export class WorkspaceResolver {
const inviteId = await this.permissions.grant( const inviteId = await this.permissions.grant(
workspaceId, workspaceId,
target.id, target.id,
permission Permission.Write
); );
if (sendInviteMail) { if (sendInviteMail) {
try { try {
+1 -2
View File
@@ -220,7 +220,6 @@ enum ErrorNames {
BLOB_QUOTA_EXCEEDED BLOB_QUOTA_EXCEEDED
CANNOT_DELETE_ALL_ADMIN_ACCOUNT CANNOT_DELETE_ALL_ADMIN_ACCOUNT
CANNOT_DELETE_OWN_ACCOUNT CANNOT_DELETE_OWN_ACCOUNT
CANT_CHANGE_SPACE_OWNER
CANT_UPDATE_ONETIME_PAYMENT_SUBSCRIPTION CANT_UPDATE_ONETIME_PAYMENT_SUBSCRIPTION
CAPTCHA_VERIFICATION_FAILED CAPTCHA_VERIFICATION_FAILED
COPILOT_ACTION_TAKEN COPILOT_ACTION_TAKEN
@@ -526,7 +525,7 @@ type Mutation {
"""Create a chat session""" """Create a chat session"""
forkCopilotSession(options: ForkChatSessionInput!): String! forkCopilotSession(options: ForkChatSessionInput!): String!
grantMember(permission: Permission!, userId: String!, workspaceId: String!): String! grantMember(permission: Permission!, userId: String!, workspaceId: String!): String!
invite(email: String!, permission: Permission!, sendInviteMail: Boolean, workspaceId: String!): String! invite(email: String!, permission: Permission @deprecated(reason: "never used"), sendInviteMail: Boolean, workspaceId: String!): String!
inviteBatch(emails: [String!]!, sendInviteMail: Boolean, workspaceId: String!): [InviteResult!]! inviteBatch(emails: [String!]!, sendInviteMail: Boolean, workspaceId: String!): [InviteResult!]!
leaveWorkspace(sendLeaveMail: Boolean, workspaceId: String!, workspaceName: String @deprecated(reason: "no longer used")): Boolean! leaveWorkspace(sendLeaveMail: Boolean, workspaceId: String!, workspaceName: String @deprecated(reason: "no longer used")): Boolean!
publishPage(mode: PublicPageMode = Page, pageId: String!, workspaceId: String!): WorkspacePage! publishPage(mode: PublicPageMode = Page, pageId: String!, workspaceId: String!): WorkspacePage!
+3 -16
View File
@@ -147,13 +147,7 @@ test('should create session correctly', async t => {
); );
}); });
const inviteId = await inviteUser( const inviteId = await inviteUser(app, token, id, 'darksky@affine.pro');
app,
token,
id,
'darksky@affine.pro',
'Admin'
);
await acceptInviteById(app, id, inviteId, false); await acceptInviteById(app, id, inviteId, false);
await assertCreateSession( await assertCreateSession(
id, id,
@@ -240,13 +234,7 @@ test('should fork session correctly', async t => {
} }
); );
const inviteId = await inviteUser( const inviteId = await inviteUser(app, token, id, 'test@affine.pro');
app,
token,
id,
'test@affine.pro',
'Admin'
);
await acceptInviteById(app, id, inviteId, false); await acceptInviteById(app, id, inviteId, false);
await assertForkSession( await assertForkSession(
newToken, newToken,
@@ -609,8 +597,7 @@ test('should reject request that user have not permission', async t => {
app, app,
anotherToken, anotherToken,
workspaceId, workspaceId,
'darksky@affine.pro', 'darksky@affine.pro'
'Admin'
); );
await acceptInviteById(app, workspaceId, inviteId, false); await acceptInviteById(app, workspaceId, inviteId, false);
+1 -8
View File
@@ -44,14 +44,7 @@ test('should send invite email', async t => {
const stub = Sinon.stub(mail, 'sendMail'); const stub = Sinon.stub(mail, 'sendMail');
await inviteUser( await inviteUser(app, u1.token.token, workspace.id, u2.email, true);
app,
u1.token.token,
workspace.id,
u2.email,
'Admin',
true
);
t.true(stub.calledOnce); t.true(stub.calledOnce);
+57 -9
View File
@@ -120,7 +120,6 @@ const init = async (
owner.token.token, owner.token.token,
workspace.id, workspace.id,
member.email, member.email,
permission,
shouldSendEmail shouldSendEmail
); );
await acceptInviteById(app, workspace.id, inviteId, shouldSendEmail); await acceptInviteById(app, workspace.id, inviteId, shouldSendEmail);
@@ -133,10 +132,16 @@ const init = async (
owner.token.token, owner.token.token,
teamWorkspace.id, teamWorkspace.id,
member.email, member.email,
permission,
shouldSendEmail shouldSendEmail
); );
await acceptInviteById(app, teamWorkspace.id, inviteId, shouldSendEmail); await acceptInviteById(app, teamWorkspace.id, inviteId, shouldSendEmail);
await grantMember(
app,
owner.token.token,
teamWorkspace.id,
member.id,
permission
);
} }
return member; return member;
@@ -437,8 +442,11 @@ test('should be able to manage invite link', async t => {
read, read,
} = await init(app, 4); } = await init(app, 4);
for (const workspace of [ws, tws]) { for (const [workspace, managers] of [
for (const manager of [owner, admin]) { [ws, [owner]],
[tws, [owner, admin]],
] as const) {
for (const manager of managers) {
const { link } = await createInviteLink( const { link } = await createInviteLink(
app, app,
manager.token.token, manager.token.token,
@@ -646,16 +654,21 @@ test('should be able to emit events', async t => {
const { teamWorkspace: tws, inviteBatch } = await init(app, 4); const { teamWorkspace: tws, inviteBatch } = await init(app, 4);
await inviteBatch(['m1@affine.pro', 'm2@affine.pro']); await inviteBatch(['m1@affine.pro', 'm2@affine.pro']);
t.true( const [membersUpdated] = event.emit
event.emit.calledOnceWith('workspace.members.updated', { .getCalls()
.map(call => call.args)
.toReversed();
t.deepEqual(membersUpdated, [
'workspace.members.updated',
{
workspaceId: tws.id, workspaceId: tws.id,
count: 6, count: 6,
}) },
); ]);
} }
{ {
const { teamWorkspace: tws, owner, createInviteLink } = await init(app, 10); const { teamWorkspace: tws, owner, createInviteLink } = await init(app);
const [, invite] = await createInviteLink(tws); const [, invite] = await createInviteLink(tws);
const user = await invite('m3@affine.pro'); const user = await invite('m3@affine.pro');
const { members } = await getWorkspace(app, owner.token.token, tws.id); const { members } = await getWorkspace(app, owner.token.token, tws.id);
@@ -679,4 +692,39 @@ test('should be able to emit events', async t => {
'should emit review requested event' 'should emit review requested event'
); );
} }
{
const { teamWorkspace: tws, owner, read } = await init(app);
await grantMember(app, owner.token.token, tws.id, read.id, 'Admin');
t.deepEqual(
event.emit.lastCall.args,
[
'workspace.members.roleChanged',
{ userId: read.id, workspaceId: tws.id, permission: Permission.Admin },
],
'should emit role changed event'
);
await grantMember(app, owner.token.token, tws.id, read.id, 'Owner');
const [ownerTransferred, roleChanged] = event.emit
.getCalls()
.map(call => call.args)
.toReversed();
t.deepEqual(
roleChanged,
[
'workspace.members.roleChanged',
{ userId: read.id, workspaceId: tws.id, permission: Permission.Owner },
],
'should emit role changed event'
);
t.deepEqual(
ownerTransferred,
[
'workspace.members.ownerTransferred',
{ email: owner.email, workspaceId: tws.id },
],
'should emit owner transferred event'
);
}
}); });
@@ -3,14 +3,12 @@ import request from 'supertest';
import type { InvitationType } from '../../src/core/workspaces'; import type { InvitationType } from '../../src/core/workspaces';
import { gql } from './common'; import { gql } from './common';
import { PermissionEnum } from './utils';
export async function inviteUser( export async function inviteUser(
app: INestApplication, app: INestApplication,
token: string, token: string,
workspaceId: string, workspaceId: string,
email: string, email: string,
permission: PermissionEnum,
sendInviteMail = false sendInviteMail = false
): Promise<string> { ): Promise<string> {
const res = await request(app.getHttpServer()) const res = await request(app.getHttpServer())
@@ -20,7 +18,7 @@ export async function inviteUser(
.send({ .send({
query: ` query: `
mutation { mutation {
invite(workspaceId: "${workspaceId}", email: "${email}", permission: ${permission}, sendInviteMail: ${sendInviteMail}) invite(workspaceId: "${workspaceId}", email: "${email}", sendInviteMail: ${sendInviteMail})
} }
`, `,
}) })
@@ -52,13 +52,7 @@ test('should invite a user', async t => {
const workspace = await createWorkspace(app, u1.token.token); const workspace = await createWorkspace(app, u1.token.token);
const invite = await inviteUser( const invite = await inviteUser(app, u1.token.token, workspace.id, u2.email);
app,
u1.token.token,
workspace.id,
u2.email,
'Admin'
);
t.truthy(invite, 'failed to invite user'); t.truthy(invite, 'failed to invite user');
}); });
@@ -68,13 +62,7 @@ test('should leave a workspace', async t => {
const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1'); const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1');
const workspace = await createWorkspace(app, u1.token.token); const workspace = await createWorkspace(app, u1.token.token);
const id = await inviteUser( const id = await inviteUser(app, u1.token.token, workspace.id, u2.email);
app,
u1.token.token,
workspace.id,
u2.email,
'Admin'
);
await acceptInviteById(app, workspace.id, id, false); await acceptInviteById(app, workspace.id, id, false);
const leave = await leaveWorkspace(app, u2.token.token, workspace.id); const leave = await leaveWorkspace(app, u2.token.token, workspace.id);
@@ -89,7 +77,7 @@ test('should revoke a user', async t => {
const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1'); const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1');
const workspace = await createWorkspace(app, u1.token.token); const workspace = await createWorkspace(app, u1.token.token);
await inviteUser(app, u1.token.token, workspace.id, u2.email, 'Admin'); await inviteUser(app, u1.token.token, workspace.id, u2.email);
const currWorkspace = await getWorkspace(app, u1.token.token, workspace.id); const currWorkspace = await getWorkspace(app, u1.token.token, workspace.id);
t.is(currWorkspace.members.length, 2, 'failed to invite user'); t.is(currWorkspace.members.length, 2, 'failed to invite user');
@@ -104,7 +92,7 @@ test('should create user if not exist', async t => {
const workspace = await createWorkspace(app, u1.token.token); const workspace = await createWorkspace(app, u1.token.token);
await inviteUser(app, u1.token.token, workspace.id, 'u2@affine.pro', 'Admin'); await inviteUser(app, u1.token.token, workspace.id, 'u2@affine.pro');
const u2 = await user.findUserByEmail('u2@affine.pro'); const u2 = await user.findUserByEmail('u2@affine.pro');
t.not(u2, undefined, 'failed to create user'); t.not(u2, undefined, 'failed to create user');
@@ -118,24 +106,12 @@ test('should invite a user by link', async t => {
const workspace = await createWorkspace(app, u1.token.token); const workspace = await createWorkspace(app, u1.token.token);
const invite = await inviteUser( const invite = await inviteUser(app, u1.token.token, workspace.id, u2.email);
app,
u1.token.token,
workspace.id,
u2.email,
'Admin'
);
const accept = await acceptInviteById(app, workspace.id, invite); const accept = await acceptInviteById(app, workspace.id, invite);
t.true(accept, 'failed to accept invite'); t.true(accept, 'failed to accept invite');
const invite1 = await inviteUser( const invite1 = await inviteUser(app, u1.token.token, workspace.id, u2.email);
app,
u1.token.token,
workspace.id,
u2.email,
'Admin'
);
t.is(invite, invite1, 'repeat the invitation must return same id'); t.is(invite, invite1, 'repeat the invitation must return same id');
@@ -159,7 +135,6 @@ test('should send email', async t => {
u1.token.token, u1.token.token,
workspace.id, workspace.id,
u2.email, u2.email,
'Admin',
true true
); );
@@ -224,20 +199,8 @@ test('should support pagination for member', async t => {
const u3 = await signUp(app, 'u3', 'u3@affine.pro', '1'); const u3 = await signUp(app, 'u3', 'u3@affine.pro', '1');
const workspace = await createWorkspace(app, u1.token.token); const workspace = await createWorkspace(app, u1.token.token);
const invite1 = await inviteUser( const invite1 = await inviteUser(app, u1.token.token, workspace.id, u2.email);
app, const invite2 = await inviteUser(app, u1.token.token, workspace.id, u3.email);
u1.token.token,
workspace.id,
u2.email,
'Admin'
);
const invite2 = await inviteUser(
app,
u1.token.token,
workspace.id,
u3.email,
'Admin'
);
await acceptInviteById(app, workspace.id, invite1, false); await acceptInviteById(app, workspace.id, invite1, false);
await acceptInviteById(app, workspace.id, invite2, false); await acceptInviteById(app, workspace.id, invite2, false);
@@ -267,13 +230,7 @@ test('should limit member count correctly', async t => {
const workspace = await createWorkspace(app, u1.token.token); const workspace = await createWorkspace(app, u1.token.token);
await Promise.allSettled( await Promise.allSettled(
Array.from({ length: 10 }).map(async (_, i) => Array.from({ length: 10 }).map(async (_, i) =>
inviteUser( inviteUser(app, u1.token.token, workspace.id, `u${i}@affine.pro`)
app,
u1.token.token,
workspace.id,
`u${i}@affine.pro`,
'Admin'
)
) )
); );
@@ -134,7 +134,7 @@ test('should share a page', async t => {
await acceptInviteById( await acceptInviteById(
app, app,
workspace.id, workspace.id,
await inviteUser(app, u1.token.token, workspace.id, u2.email, 'Admin') await inviteUser(app, u1.token.token, workspace.id, u2.email)
); );
const invited = await publishPage(app, u2.token.token, workspace.id, 'page2'); const invited = await publishPage(app, u2.token.token, workspace.id, 'page2');
t.is(invited.id, 'page2', 'failed to share page'); t.is(invited.id, 'page2', 'failed to share page');
@@ -211,7 +211,7 @@ test('should can get workspace doc', async t => {
await acceptInviteById( await acceptInviteById(
app, app,
workspace.id, workspace.id,
await inviteUser(app, u1.token.token, workspace.id, u2.email, 'Admin') await inviteUser(app, u1.token.token, workspace.id, u2.email)
); );
const res2 = await request(app.getHttpServer()) const res2 = await request(app.getHttpServer())
@@ -75,15 +75,10 @@ export class WorkspacePermission extends Entity {
}) })
); );
async inviteMember( async inviteMember(email: string, sendInviteMail?: boolean) {
email: string,
permission: Permission,
sendInviteMail?: boolean
) {
return await this.store.inviteMember( return await this.store.inviteMember(
this.workspaceService.workspace.id, this.workspaceService.workspace.id,
email, email,
permission,
sendInviteMail sendInviteMail
); );
} }
@@ -38,7 +38,6 @@ export class WorkspacePermissionStore extends Store {
async inviteMember( async inviteMember(
workspaceId: string, workspaceId: string,
email: string, email: string,
permission: Permission,
sendInviteMail = false sendInviteMail = false
) { ) {
if (!this.workspaceServerService.server) { if (!this.workspaceServerService.server) {
@@ -49,7 +48,6 @@ export class WorkspacePermissionStore extends Store {
variables: { variables: {
workspaceId, workspaceId,
email, email,
permission,
sendInviteMail, sendInviteMail,
}, },
}); });
@@ -1367,11 +1367,10 @@ export const inviteByEmailMutation = {
definitionName: 'invite', definitionName: 'invite',
containsFile: false, containsFile: false,
query: ` query: `
mutation inviteByEmail($workspaceId: String!, $email: String!, $permission: Permission!, $sendInviteMail: Boolean) { mutation inviteByEmail($workspaceId: String!, $email: String!, $sendInviteMail: Boolean) {
invite( invite(
workspaceId: $workspaceId workspaceId: $workspaceId
email: $email email: $email
permission: $permission
sendInviteMail: $sendInviteMail sendInviteMail: $sendInviteMail
) )
}`, }`,
@@ -1,13 +1,11 @@
mutation inviteByEmail( mutation inviteByEmail(
$workspaceId: String! $workspaceId: String!
$email: String! $email: String!
$permission: Permission!
$sendInviteMail: Boolean $sendInviteMail: Boolean
) { ) {
invite( invite(
workspaceId: $workspaceId workspaceId: $workspaceId
email: $email email: $email
permission: $permission
sendInviteMail: $sendInviteMail sendInviteMail: $sendInviteMail
) )
} }
+2 -4
View File
@@ -1,4 +1,4 @@
/* eslint-disable */
export type Maybe<T> = T | null; export type Maybe<T> = T | null;
export type InputMaybe<T> = T | null; export type InputMaybe<T> = T | null;
export type Exact<T extends { [key: string]: unknown }> = { export type Exact<T extends { [key: string]: unknown }> = {
@@ -298,7 +298,6 @@ export enum ErrorNames {
BLOB_QUOTA_EXCEEDED = 'BLOB_QUOTA_EXCEEDED', BLOB_QUOTA_EXCEEDED = 'BLOB_QUOTA_EXCEEDED',
CANNOT_DELETE_ALL_ADMIN_ACCOUNT = 'CANNOT_DELETE_ALL_ADMIN_ACCOUNT', CANNOT_DELETE_ALL_ADMIN_ACCOUNT = 'CANNOT_DELETE_ALL_ADMIN_ACCOUNT',
CANNOT_DELETE_OWN_ACCOUNT = 'CANNOT_DELETE_OWN_ACCOUNT', CANNOT_DELETE_OWN_ACCOUNT = 'CANNOT_DELETE_OWN_ACCOUNT',
CANT_CHANGE_SPACE_OWNER = 'CANT_CHANGE_SPACE_OWNER',
CANT_UPDATE_ONETIME_PAYMENT_SUBSCRIPTION = 'CANT_UPDATE_ONETIME_PAYMENT_SUBSCRIPTION', CANT_UPDATE_ONETIME_PAYMENT_SUBSCRIPTION = 'CANT_UPDATE_ONETIME_PAYMENT_SUBSCRIPTION',
CAPTCHA_VERIFICATION_FAILED = 'CAPTCHA_VERIFICATION_FAILED', CAPTCHA_VERIFICATION_FAILED = 'CAPTCHA_VERIFICATION_FAILED',
COPILOT_ACTION_TAKEN = 'COPILOT_ACTION_TAKEN', COPILOT_ACTION_TAKEN = 'COPILOT_ACTION_TAKEN',
@@ -729,7 +728,7 @@ export interface MutationGrantMemberArgs {
export interface MutationInviteArgs { export interface MutationInviteArgs {
email: Scalars['String']['input']; email: Scalars['String']['input'];
permission: Permission; permission?: InputMaybe<Permission>;
sendInviteMail?: InputMaybe<Scalars['Boolean']['input']>; sendInviteMail?: InputMaybe<Scalars['Boolean']['input']>;
workspaceId: Scalars['String']['input']; workspaceId: Scalars['String']['input'];
} }
@@ -2647,7 +2646,6 @@ export type RemoveWorkspaceFeatureMutation = {
export type InviteByEmailMutationVariables = Exact<{ export type InviteByEmailMutationVariables = Exact<{
workspaceId: Scalars['String']['input']; workspaceId: Scalars['String']['input'];
email: Scalars['String']['input']; email: Scalars['String']['input'];
permission: Permission;
sendInviteMail?: InputMaybe<Scalars['Boolean']['input']>; sendInviteMail?: InputMaybe<Scalars['Boolean']['input']>;
}>; }>;