From adb9ac8fb0c91a8a6dd453ee13fa75d238c92d99 Mon Sep 17 00:00:00 2001 From: DarkSky Date: Fri, 20 Dec 2024 17:03:12 +0800 Subject: [PATCH] feat: role changed email --- packages/backend/server/src/base/event/def.ts | 6 ++ .../server/src/base/mailer/mail.service.ts | 25 ++++++- .../server/src/base/mailer/template.ts | 35 ++++++++++ .../src/core/workspaces/resolvers/service.ts | 65 +++++++++++++------ .../src/core/workspaces/resolvers/team.ts | 38 ++++++++++- 5 files changed, 147 insertions(+), 22 deletions(-) diff --git a/packages/backend/server/src/base/event/def.ts b/packages/backend/server/src/base/event/def.ts index 910443480e767..7f253d9343a59 100644 --- a/packages/backend/server/src/base/event/def.ts +++ b/packages/backend/server/src/base/event/def.ts @@ -10,6 +10,12 @@ export interface WorkspaceEvents { workspaceId: Workspace['id']; }>; 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 }>; }; deleted: Payload; diff --git a/packages/backend/server/src/base/mailer/mail.service.ts b/packages/backend/server/src/base/mailer/mail.service.ts index cf1f1cde21613..f078851fe6b62 100644 --- a/packages/backend/server/src/base/mailer/mail.service.ts +++ b/packages/backend/server/src/base/mailer/mail.service.ts @@ -6,7 +6,12 @@ import { URLHelper } from '../helpers'; import { metrics } from '../metrics'; import type { MailerService, Options } from './mailer'; import { MAILER_SERVICE } from './mailer'; -import { emailTemplate } from './template'; +import { + emailTemplate, + getRoleChangedTemplate, + type RoleChangedMailParams, +} from './template'; + @Injectable() export class MailService { constructor( @@ -311,4 +316,22 @@ export class MailService { }); 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 }); + } } diff --git a/packages/backend/server/src/base/mailer/template.ts b/packages/backend/server/src/base/mailer/template.ts index 3e50a317187e7..d790adfcc92a9 100644 --- a/packages/backend/server/src/base/mailer/template.ts +++ b/packages/backend/server/src/base/mailer/template.ts @@ -219,3 +219,38 @@ export const emailTemplate = ({ `; }; + +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 }; +}; diff --git a/packages/backend/server/src/core/workspaces/resolvers/service.ts b/packages/backend/server/src/core/workspaces/resolvers/service.ts index 3743608be32c1..14cf1bb64a636 100644 --- a/packages/backend/server/src/core/workspaces/resolvers/service.ts +++ b/packages/backend/server/src/core/workspaces/resolvers/service.ts @@ -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 }); } } diff --git a/packages/backend/server/src/core/workspaces/resolvers/team.ts b/packages/backend/server/src/core/workspaces/resolvers/team.ts index ab0f1d5069654..bf09d71df1d74 100644 --- a/packages/backend/server/src/core/workspaces/resolvers/team.ts +++ b/packages/backend/server/src/core/workspaces/resolvers/team.ts @@ -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,7 @@ export class TeamWorkspaceResolver { } } catch (e) { this.logger.error('failed to invite user', e); + if (e instanceof UserFriendlyError) return e; return new TooManyRequest(); } } @@ -353,4 +365,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, + }); + } }