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

View File

@@ -322,10 +322,6 @@ export class PermissionService {
this.prisma.workspaceUserPermission.update({
where: {
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 },
}),

View File

@@ -2,9 +2,9 @@ import { Injectable, Logger } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { getStreamAsBuffer } from 'get-stream';
import { Cache, MailService } from '../../../base';
import { Cache, MailService, UserNotFound } from '../../../base';
import { DocContentService } from '../../doc-renderer';
import { PermissionService } from '../../permission';
import { Permission, PermissionService } from '../../permission';
import { WorkspaceBlobStorage } from '../../storage';
import { UserService } from '../../user';
@@ -17,6 +17,13 @@ export type InviteInfo = {
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()
export class WorkspaceService {
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) {
const { workspaceId, inviterUserId, inviteeUserId } =
await this.getInviteInfo(inviteId);
@@ -167,24 +195,21 @@ export class WorkspaceService {
await this.mailer.sendReviewDeclinedEmail(email, { name: workspaceName });
}
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;
}
async sendRoleChangedEmail(
userId: string,
ws: { id: string; role: Permission }
) {
const user = await this.user.findUserById(userId);
if (!user) throw new UserNotFound();
const workspace = await this.getWorkspaceInfo(ws.id);
await this.mailer.sendRoleChangedEmail(user?.email, {
name: workspace.name,
role: PermissionToRole[ws.role],
});
}
return {
email: invitee.email,
workspace,
};
async sendOwnerTransferred(email: string, ws: { id: string }) {
const workspace = await this.getWorkspaceInfo(ws.id);
await this.mailer.sendOwnerTransferred(email, { name: workspace.name });
}
}

View File

@@ -18,6 +18,7 @@ import {
RequestMutex,
TooManyRequest,
URLHelper,
UserFriendlyError,
} from '../../../base';
import { CurrentUser } from '../../auth';
import { Permission, PermissionService } from '../../permission';
@@ -311,7 +312,17 @@ export class TeamWorkspaceResolver {
);
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;
@@ -320,6 +331,10 @@ export class TeamWorkspaceResolver {
}
} catch (e) {
this.logger.error('failed to invite user', e);
// pass through user friendly error
if (e instanceof UserFriendlyError) {
return e;
}
return new TooManyRequest();
}
}
@@ -353,4 +368,28 @@ export class TeamWorkspaceResolver {
// send approve mail
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,
});
}
}

View File

@@ -17,7 +17,6 @@ import type { FileUpload } from '../../../base';
import {
AlreadyInSpace,
Cache,
CantChangeSpaceOwner,
DocNotFound,
EventEmitter,
InternalServerError,
@@ -383,8 +382,13 @@ export class WorkspaceResolver {
@CurrentUser() user: CurrentUser,
@Args('workspaceId') workspaceId: 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(
workspaceId,
@@ -392,10 +396,6 @@ export class WorkspaceResolver {
Permission.Admin
);
if (permission === Permission.Owner) {
throw new CantChangeSpaceOwner();
}
try {
// lock to prevent concurrent invite and grant
const lockFlag = `invite:${workspaceId}`;
@@ -428,7 +428,7 @@ export class WorkspaceResolver {
const inviteId = await this.permissions.grant(
workspaceId,
target.id,
permission
Permission.Write
);
if (sendInviteMail) {
try {