mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-07 09:21:24 +08:00
feat(server): role changed email (#9227)
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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!
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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']>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user