feat(server): new email template (#9528)

use `yarn af server dev:mail` to preview all mail template
fix CLOUD-93
This commit is contained in:
darkskygit
2025-01-22 02:55:24 +00:00
parent 2db9cc3922
commit 83ed215f4a
54 changed files with 4794 additions and 892 deletions
@@ -302,31 +302,29 @@ export class AuthService implements OnApplicationBootstrap {
}
async sendChangePasswordEmail(email: string, callbackUrl: string) {
return this.mailer.sendChangePasswordEmail(email, callbackUrl);
return this.mailer.sendChangePasswordMail(email, { url: callbackUrl });
}
async sendSetPasswordEmail(email: string, callbackUrl: string) {
return this.mailer.sendSetPasswordEmail(email, callbackUrl);
return this.mailer.sendSetPasswordMail(email, { url: callbackUrl });
}
async sendChangeEmail(email: string, callbackUrl: string) {
return this.mailer.sendChangeEmail(email, callbackUrl);
return this.mailer.sendChangeEmailMail(email, { url: callbackUrl });
}
async sendVerifyChangeEmail(email: string, callbackUrl: string) {
return this.mailer.sendVerifyChangeEmail(email, callbackUrl);
return this.mailer.sendVerifyChangeEmail(email, { url: callbackUrl });
}
async sendVerifyEmail(email: string, callbackUrl: string) {
return this.mailer.sendVerifyEmail(email, callbackUrl);
return this.mailer.sendVerifyEmail(email, { url: callbackUrl });
}
async sendNotificationChangeEmail(email: string) {
return this.mailer.sendNotificationChangeEmail(email);
return this.mailer.sendNotificationChangeEmail(email, {
to: email,
});
}
async sendSignInEmail(email: string, link: string, signUp: boolean) {
return signUp
? await this.mailer.sendSignUpMail(link, {
to: email,
})
: await this.mailer.sendSignInMail(link, {
to: email,
});
? await this.mailer.sendSignUpMail(email, { url: link })
: await this.mailer.sendSignInMail(email, { url: link });
}
}
@@ -7,6 +7,7 @@ import {
type EventPayload,
MailService,
OnEvent,
URLHelper,
UserNotFound,
} from '../../../base';
import { Models } from '../../../models';
@@ -23,13 +24,6 @@ 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);
@@ -41,7 +35,8 @@ export class WorkspaceService {
private readonly mailer: MailService,
private readonly permission: PermissionService,
private readonly prisma: PrismaClient,
private readonly models: Models
private readonly models: Models,
private readonly url: URLHelper
) {}
async getInviteInfo(inviteId: string): Promise<InviteInfo> {
@@ -87,7 +82,7 @@ export class WorkspaceService {
return {
avatar,
id: workspaceId,
name: workspaceContent?.name ?? '',
name: workspaceContent?.name ?? 'Untitled Workspace',
};
}
@@ -130,26 +125,47 @@ export class WorkspaceService {
return false;
}
await this.mailer.sendAcceptedEmail(inviter.email, {
inviteeName: invitee.name,
workspaceName: workspace.name,
await this.mailer.sendMemberAcceptedEmail(inviter.email, {
user: invitee,
workspace,
});
return true;
}
async sendInviteEmail(inviteId: string) {
const target = await this.getInviteeEmailTarget(inviteId);
if (!target) {
return;
}
const owner = await this.permission.getWorkspaceOwner(target.workspace.id);
await this.mailer.sendMemberInviteMail(target.email, {
workspace: target.workspace,
user: owner,
url: this.url.link(`/invite/${inviteId}`),
});
}
// ================ Team ================
async sendTeamWorkspaceUpgradedEmail(workspaceId: string) {
const workspace = await this.getWorkspaceInfo(workspaceId);
const owner = await this.permission.getWorkspaceOwner(workspaceId);
const admin = await this.permission.getWorkspaceAdmin(workspaceId);
const admins = await this.permission.getWorkspaceAdmin(workspaceId);
await this.mailer.sendTeamWorkspaceUpgradedEmail(owner.email, {
...workspace,
workspace,
isOwner: true,
url: this.url.link(`/workspace/${workspaceId}`),
});
for (const user of admin) {
for (const user of admins) {
await this.mailer.sendTeamWorkspaceUpgradedEmail(user.email, {
...workspace,
workspace,
isOwner: false,
url: this.url.link(`/workspace/${workspaceId}`),
});
}
}
@@ -174,48 +190,34 @@ export class WorkspaceService {
const admin = await this.permission.getWorkspaceAdmin(workspaceId);
for (const user of [owner, ...admin]) {
await this.mailer.sendReviewRequestEmail(
user.email,
invitee.email,
workspace
);
await this.mailer.sendLinkInvitationReviewRequestMail(user.email, {
workspace,
user: invitee,
url: this.url.link(`/workspace/${workspace.id}`),
});
}
}
async sendInviteEmail(inviteId: string) {
const target = await this.getInviteeEmailTarget(inviteId);
if (!target) {
return;
}
const owner = await this.permission.getWorkspaceOwner(target.workspace.id);
await this.mailer.sendInviteEmail(target.email, inviteId, {
workspace: target.workspace,
user: {
avatar: owner.avatarUrl || '',
name: owner.name || '',
},
});
}
async sendReviewApproveEmail(inviteId: string) {
const target = await this.getInviteeEmailTarget(inviteId);
if (!target) return;
if (!target) {
return;
}
await this.mailer.sendReviewApproveEmail(target.email, target.workspace);
await this.mailer.sendLinkInvitationApproveMail(target.email, {
workspace: target.workspace,
url: this.url.link(`/workspace/${target.workspace.id}`),
});
}
async sendReviewDeclinedEmail(
email: string | undefined,
workspaceName: string
workspaceId: string
) {
if (!email) return;
await this.mailer.sendReviewDeclinedEmail(email, { name: workspaceName });
const workspace = await this.getWorkspaceInfo(workspaceId);
await this.mailer.sendLinkInvitationDeclineMail(email, {
workspace,
});
}
async sendRoleChangedEmail(
@@ -224,24 +226,42 @@ export class WorkspaceService {
) {
const user = await this.models.user.getPublicUser(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],
});
if (ws.role === Permission.Admin) {
await this.mailer.sendTeamBecomeAdminMail(user.email, {
workspace,
url: this.url.link(`/workspace/${workspace.id}`),
});
} else {
await this.mailer.sendTeamBecomeCollaboratorMail(user.email, {
workspace,
url: this.url.link(`/workspace/${workspace.id}`),
});
}
}
async sendOwnerTransferredEmail(email: string, ws: { id: string }) {
async sendOwnershipTransferredEmail(email: string, ws: { id: string }) {
const workspace = await this.getWorkspaceInfo(ws.id);
await this.mailer.sendOwnershipTransferredEmail(email, {
name: workspace.name,
});
await this.mailer.sendOwnershipTransferredMail(email, { workspace });
}
async sendMemberRemoved(email: string, ws: { id: string }) {
async sendOwnershipReceivedEmail(email: string, ws: { id: string }) {
const workspace = await this.getWorkspaceInfo(ws.id);
await this.mailer.sendMemberRemovedEmail(email, {
name: workspace.name,
await this.mailer.sendOwnershipReceivedMail(email, { workspace });
}
@OnEvent('workspace.members.leave')
async onMemberLeave({
user,
workspaceId,
}: EventPayload<'workspace.members.leave'>) {
const workspace = await this.getWorkspaceInfo(workspaceId);
const owner = await this.permission.getWorkspaceOwner(workspaceId);
await this.mailer.sendMemberLeaveEmail(owner.email, {
workspace,
user,
});
}
@@ -252,22 +272,8 @@ export class WorkspaceService {
}: EventPayload<'workspace.members.requestDeclined'>) {
const user = await this.models.user.get(userId);
if (!user) return;
await this.sendMemberRemoved(user.email, { id: workspaceId });
}
@OnEvent('workspace.subscription.notify')
async onSubscriptionNotify({
workspaceId,
expirationDate,
deletionDate,
}: EventPayload<'workspace.subscription.notify'>) {
const owner = await this.permission.getWorkspaceOwner(workspaceId);
if (!owner) return;
const workspace = await this.getWorkspaceInfo(workspaceId);
await this.mailer.sendWorkspaceExpireRemindEmail(owner.email, {
...workspace,
expirationDate,
deletionDate,
});
await this.mailer.sendMemberRemovedMail(user.email, { workspace });
}
}
@@ -312,15 +312,17 @@ export class TeamWorkspaceResolver {
);
if (result) {
this.event.emit('workspace.members.roleChanged', {
userId,
workspaceId,
permission,
});
if (permission === Permission.Owner) {
this.event.emit('workspace.members.ownerTransferred', {
email: user.email,
this.event.emit('workspace.members.ownershipTransferred', {
workspaceId,
from: user.id,
to: userId,
});
} else {
this.event.emit('workspace.members.roleChanged', {
userId,
workspaceId,
permission,
});
}
}
@@ -347,20 +349,6 @@ export class TeamWorkspaceResolver {
await this.workspaceService.sendReviewRequestedEmail(inviteId);
}
@OnEvent('workspace.members.requestDeclined')
async onDeclineRequest({
userId,
workspaceId,
}: EventPayload<'workspace.members.requestDeclined'>) {
const user = await this.models.user.getPublicUser(userId);
const workspace = await this.workspaceService.getWorkspaceInfo(workspaceId);
// send decline mail
await this.workspaceService.sendReviewDeclinedEmail(
user?.email,
workspace.name
);
}
@OnEvent('workspace.members.requestApproved')
async onApproveRequest({
inviteId,
@@ -369,6 +357,19 @@ export class TeamWorkspaceResolver {
await this.workspaceService.sendReviewApproveEmail(inviteId);
}
@OnEvent('workspace.members.requestDeclined')
async onDeclineRequest({
userId,
workspaceId,
}: EventPayload<'workspace.members.requestDeclined'>) {
const user = await this.models.user.getPublicUser(userId);
// send decline mail
await this.workspaceService.sendReviewDeclinedEmail(
user?.email,
workspaceId
);
}
@OnEvent('workspace.members.roleChanged')
async onRoleChanged({
userId,
@@ -382,14 +383,29 @@ export class TeamWorkspaceResolver {
});
}
@OnEvent('workspace.members.ownerTransferred')
@OnEvent('workspace.members.ownershipTransferred')
async onOwnerTransferred({
email,
workspaceId,
}: EventPayload<'workspace.members.ownerTransferred'>) {
// send role changed mail
await this.workspaceService.sendOwnerTransferredEmail(email, {
id: workspaceId,
});
from,
to,
}: EventPayload<'workspace.members.ownershipTransferred'>) {
// send ownership transferred mail
const fromUser = await this.models.user.getPublicUser(from);
const toUser = await this.models.user.getPublicUser(to);
if (fromUser) {
await this.workspaceService.sendOwnershipTransferredEmail(
fromUser.email,
{
id: workspaceId,
}
);
}
if (toUser) {
await this.workspaceService.sendOwnershipReceivedEmail(toUser.email, {
id: workspaceId,
});
}
}
}
@@ -20,7 +20,6 @@ import {
DocNotFound,
EventEmitter,
InternalServerError,
MailService,
MemberQuotaExceeded,
RequestMutex,
SpaceAccessDenied,
@@ -79,7 +78,6 @@ export class WorkspaceResolver {
constructor(
private readonly cache: Cache,
private readonly mailer: MailService,
private readonly prisma: PrismaClient,
private readonly permissions: PermissionService,
private readonly quota: QuotaManagementService,
@@ -611,17 +609,18 @@ export class WorkspaceResolver {
_workspaceName?: string
) {
await this.permissions.checkWorkspace(workspaceId, user.id);
const { name: workspaceName } =
await this.workspaceService.getWorkspaceInfo(workspaceId);
const owner = await this.permissions.getWorkspaceOwner(workspaceId);
const success = this.permissions.revokeWorkspace(workspaceId, user.id);
if (sendLeaveMail) {
await this.mailer.sendLeaveWorkspaceEmail(owner.email, {
workspaceName,
inviteeName: user.name,
this.event.emit('workspace.members.leave', {
workspaceId,
user: {
id: user.id,
email: user.email,
},
});
}
return this.permissions.revokeWorkspace(workspaceId, user.id);
return success;
}
}