mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-22 12:36:24 +08:00
refactor(infra): directory structure (#4615)
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
export { MailService } from './mail.service';
|
||||
export { MAILER } from './mailer';
|
||||
@@ -0,0 +1,236 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
|
||||
import { Config } from '../../../config';
|
||||
import {
|
||||
MAILER_SERVICE,
|
||||
type MailerService,
|
||||
type Options,
|
||||
type Response,
|
||||
} from './mailer';
|
||||
import { emailTemplate } from './template';
|
||||
@Injectable()
|
||||
export class MailService {
|
||||
constructor(
|
||||
@Inject(MAILER_SERVICE) private readonly mailer: MailerService,
|
||||
private readonly config: Config
|
||||
) {}
|
||||
|
||||
async sendMail(options: Options): Promise<Response> {
|
||||
return this.mailer.sendMail(options);
|
||||
}
|
||||
|
||||
hasConfigured() {
|
||||
return (
|
||||
!!this.config.auth.email.login &&
|
||||
!!this.config.auth.email.password &&
|
||||
!!this.config.auth.email.sender
|
||||
);
|
||||
}
|
||||
|
||||
async sendInviteEmail(
|
||||
to: string,
|
||||
inviteId: string,
|
||||
invitationInfo: {
|
||||
workspace: {
|
||||
id: string;
|
||||
name: string;
|
||||
avatar: string;
|
||||
};
|
||||
user: {
|
||||
avatar: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
) {
|
||||
// TODO: use callback url when need support desktop app
|
||||
const buttonUrl = `${this.config.origin}/invite/${inviteId}`;
|
||||
const workspaceAvatar = invitationInfo.workspace.avatar;
|
||||
|
||||
const content = `<p style="margin:0">${
|
||||
invitationInfo.user.avatar
|
||||
? `<img
|
||||
src="${invitationInfo.user.avatar}"
|
||||
alt=""
|
||||
width="24px"
|
||||
height="24px"
|
||||
style="width:24px; height:24px; border-radius: 12px;object-fit: cover;vertical-align: middle"
|
||||
/>`
|
||||
: ''
|
||||
}
|
||||
<span style="font-weight:500;margin-right: 4px;">${
|
||||
invitationInfo.user.name
|
||||
}</span>
|
||||
<span>invited you to join</span>
|
||||
<img
|
||||
src="cid:workspaceAvatar"
|
||||
alt=""
|
||||
width="24px"
|
||||
height="24px"
|
||||
style="width:24px; height:24px; margin-left:4px;border-radius: 12px;object-fit: cover;vertical-align: middle"
|
||||
/>
|
||||
<span style="font-weight:500;margin-right: 4px;">${
|
||||
invitationInfo.workspace.name
|
||||
}</span></p><p style="margin-top:8px;margin-bottom:0;">Click button to join this workspace</p>`;
|
||||
|
||||
const subContent =
|
||||
'Currently, AFFiNE Cloud is in the early access stage. Only Early Access Sponsors can register and log in to AFFiNE Cloud. <a href="https://community.affine.pro/c/insider-general/" style="color: #1e67af" >Please click here for more information.</a>';
|
||||
|
||||
const html = emailTemplate({
|
||||
title: 'You are invited!',
|
||||
content,
|
||||
buttonContent: 'Accept & Join',
|
||||
buttonUrl,
|
||||
subContent,
|
||||
});
|
||||
|
||||
return this.sendMail({
|
||||
from: this.config.auth.email.sender,
|
||||
to,
|
||||
subject: `${invitationInfo.user.name} invited you to join ${invitationInfo.workspace.name}`,
|
||||
html,
|
||||
attachments: [
|
||||
{
|
||||
cid: 'workspaceAvatar',
|
||||
filename: 'image.png',
|
||||
content: workspaceAvatar,
|
||||
encoding: 'base64',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async sendSignInEmail(url: string, options: Options) {
|
||||
const html = emailTemplate({
|
||||
title: 'Sign in to AFFiNE',
|
||||
content:
|
||||
'Click the button below to securely sign in. The magic link will expire in 30 minutes.',
|
||||
buttonContent: 'Sign in to AFFiNE',
|
||||
buttonUrl: url,
|
||||
});
|
||||
return this.sendMail({
|
||||
html,
|
||||
subject: 'Sign in to AFFiNE',
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
async sendChangePasswordEmail(to: string, url: string) {
|
||||
const html = emailTemplate({
|
||||
title: 'Modify your AFFiNE password',
|
||||
content:
|
||||
'Click the button below to reset your password. The magic link will expire in 30 minutes.',
|
||||
buttonContent: 'Set new password',
|
||||
buttonUrl: url,
|
||||
});
|
||||
return this.sendMail({
|
||||
from: this.config.auth.email.sender,
|
||||
to,
|
||||
subject: `Modify your AFFiNE password`,
|
||||
html,
|
||||
});
|
||||
}
|
||||
|
||||
async sendSetPasswordEmail(to: string, url: string) {
|
||||
const html = emailTemplate({
|
||||
title: 'Set your AFFiNE password',
|
||||
content:
|
||||
'Click the button below to set your password. The magic link will expire in 30 minutes.',
|
||||
buttonContent: 'Set your password',
|
||||
buttonUrl: url,
|
||||
});
|
||||
return this.sendMail({
|
||||
from: this.config.auth.email.sender,
|
||||
to,
|
||||
subject: `Set your AFFiNE password`,
|
||||
html,
|
||||
});
|
||||
}
|
||||
async sendChangeEmail(to: string, url: string) {
|
||||
const html = emailTemplate({
|
||||
title: 'Verify your current email for AFFiNE',
|
||||
content:
|
||||
'You recently requested to change the email address associated with your AFFiNE account. To complete this process, please click on the verification link below. This magic link will expire in 30 minutes.',
|
||||
buttonContent: 'Verify and set up a new email address',
|
||||
buttonUrl: url,
|
||||
});
|
||||
return this.sendMail({
|
||||
from: this.config.auth.email.sender,
|
||||
to,
|
||||
subject: `Verify your current email for AFFiNE`,
|
||||
html,
|
||||
});
|
||||
}
|
||||
async sendVerifyChangeEmail(to: string, url: string) {
|
||||
const html = emailTemplate({
|
||||
title: 'Verify your new email address',
|
||||
content:
|
||||
'You recently requested to change the email address associated with your AFFiNE account. To complete this process, please click on the verification link below. This magic link will expire in 30 minutes.',
|
||||
buttonContent: 'Verify your new email address',
|
||||
buttonUrl: url,
|
||||
});
|
||||
return this.sendMail({
|
||||
from: this.config.auth.email.sender,
|
||||
to,
|
||||
subject: `Verify your new email for AFFiNE`,
|
||||
html,
|
||||
});
|
||||
}
|
||||
async sendNotificationChangeEmail(to: string) {
|
||||
const html = emailTemplate({
|
||||
title: 'Email change successful',
|
||||
content: `As per your request, we have changed your email. Please make sure you're using ${to} when you log in the next time. `,
|
||||
});
|
||||
return this.sendMail({
|
||||
from: this.config.auth.email.sender,
|
||||
to,
|
||||
subject: `Your email has been changed`,
|
||||
html,
|
||||
});
|
||||
}
|
||||
async sendAcceptedEmail(
|
||||
to: string,
|
||||
{
|
||||
inviteeName,
|
||||
workspaceName,
|
||||
}: {
|
||||
inviteeName: string;
|
||||
workspaceName: string;
|
||||
}
|
||||
) {
|
||||
const title = `${inviteeName} accepted your invitation`;
|
||||
|
||||
const html = emailTemplate({
|
||||
title,
|
||||
content: `${inviteeName} has joined ${workspaceName}`,
|
||||
});
|
||||
return this.sendMail({
|
||||
from: this.config.auth.email.sender,
|
||||
to,
|
||||
subject: title,
|
||||
html,
|
||||
});
|
||||
}
|
||||
async sendLeaveWorkspaceEmail(
|
||||
to: string,
|
||||
{
|
||||
inviteeName,
|
||||
workspaceName,
|
||||
}: {
|
||||
inviteeName: string;
|
||||
workspaceName: string;
|
||||
}
|
||||
) {
|
||||
const title = `${inviteeName} left ${workspaceName}`;
|
||||
|
||||
const html = emailTemplate({
|
||||
title,
|
||||
content: `${inviteeName} has left your workspace`,
|
||||
});
|
||||
return this.sendMail({
|
||||
from: this.config.auth.email.sender,
|
||||
to,
|
||||
subject: title,
|
||||
html,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { FactoryProvider } from '@nestjs/common';
|
||||
import { createTransport, Transporter } from 'nodemailer';
|
||||
import SMTPTransport from 'nodemailer/lib/smtp-transport';
|
||||
|
||||
import { Config } from '../../../config';
|
||||
|
||||
export const MAILER_SERVICE = Symbol('MAILER_SERVICE');
|
||||
|
||||
export type MailerService = Transporter<SMTPTransport.SentMessageInfo>;
|
||||
export type Response = SMTPTransport.SentMessageInfo;
|
||||
export type Options = SMTPTransport.Options;
|
||||
|
||||
export const MAILER: FactoryProvider<
|
||||
Transporter<SMTPTransport.SentMessageInfo>
|
||||
> = {
|
||||
provide: MAILER_SERVICE,
|
||||
useFactory: (config: Config) => {
|
||||
if (config.auth.localEmail) {
|
||||
return createTransport({
|
||||
host: '0.0.0.0',
|
||||
port: 1025,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: config.auth.email.login,
|
||||
pass: config.auth.email.password,
|
||||
},
|
||||
});
|
||||
}
|
||||
return createTransport({
|
||||
service: 'gmail',
|
||||
auth: {
|
||||
user: config.auth.email.login,
|
||||
pass: config.auth.email.password,
|
||||
},
|
||||
});
|
||||
},
|
||||
inject: [Config],
|
||||
};
|
||||
@@ -0,0 +1,221 @@
|
||||
export const emailTemplate = ({
|
||||
title,
|
||||
content,
|
||||
buttonContent,
|
||||
buttonUrl,
|
||||
subContent,
|
||||
}: {
|
||||
title: string;
|
||||
content: string;
|
||||
buttonContent?: string;
|
||||
buttonUrl?: string;
|
||||
subContent?: string;
|
||||
}) => {
|
||||
return `<body style="background: #f6f7fb; overflow: hidden">
|
||||
<table
|
||||
width="100%"
|
||||
border="0"
|
||||
cellpadding="24px"
|
||||
style="
|
||||
background: #fff;
|
||||
max-width: 450px;
|
||||
margin: 32px auto 0 auto;
|
||||
border-radius: 16px 16px 0 0;
|
||||
box-shadow: 0px 0px 20px 0px rgba(66, 65, 73, 0.04);
|
||||
"
|
||||
>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://affine.pro" target="_blank">
|
||||
<img
|
||||
src="https://cdn.affine.pro/mail/2023-8-9/affine-logo.png"
|
||||
alt="AFFiNE log"
|
||||
height="32px"
|
||||
/>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 28px;
|
||||
font-family: inter, Arial, Helvetica, sans-serif;
|
||||
color: #444;
|
||||
padding-top: 0;
|
||||
"
|
||||
>${title}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
font-family: inter, Arial, Helvetica, sans-serif;
|
||||
color: #444;
|
||||
padding-top: 0;
|
||||
"
|
||||
>${content}</td>
|
||||
</tr>
|
||||
${
|
||||
buttonContent && buttonUrl
|
||||
? `<tr>
|
||||
<td style="margin-left: 24px; padding-top: 0; padding-bottom: ${
|
||||
subContent ? '0' : '64px'
|
||||
}">
|
||||
<table border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td style="border-radius: 8px" bgcolor="#1E96EB">
|
||||
<a
|
||||
href="${buttonUrl}"
|
||||
target="_blank"
|
||||
style="
|
||||
font-size: 15px;
|
||||
font-family: inter, Arial, Helvetica, sans-serif;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 18px;
|
||||
border: 1px solid rgba(0,0,0,.1);
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
"
|
||||
>${buttonContent}</a
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>`
|
||||
: ''
|
||||
}
|
||||
${
|
||||
subContent
|
||||
? `<tr>
|
||||
<td
|
||||
style="
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
font-family: inter, Arial, Helvetica, sans-serif;
|
||||
color: #444;
|
||||
padding-top: 24px;
|
||||
"
|
||||
>
|
||||
${subContent}
|
||||
</td>
|
||||
</tr>`
|
||||
: ''
|
||||
}
|
||||
</table>
|
||||
<table
|
||||
width="100%"
|
||||
border="0"
|
||||
style="
|
||||
background: #fafafa;
|
||||
max-width: 450px;
|
||||
margin: 0 auto 32px auto;
|
||||
border-radius: 0 0 16px 16px;
|
||||
box-shadow: 0px 0px 20px 0px rgba(66, 65, 73, 0.04);
|
||||
padding: 20px;
|
||||
"
|
||||
>
|
||||
<tr align="center">
|
||||
<td>
|
||||
<table cellpadding="0">
|
||||
<tr>
|
||||
<td style="padding: 0 10px">
|
||||
<a
|
||||
href="https://github.com/toeverything/AFFiNE"
|
||||
target="_blank"
|
||||
><img
|
||||
src="https://cdn.affine.pro/mail/2023-8-9/Github.png"
|
||||
alt="AFFiNE github link"
|
||||
height="16px"
|
||||
/></a>
|
||||
</td>
|
||||
<td style="padding: 0 10px">
|
||||
<a href="https://twitter.com/AffineOfficial" target="_blank">
|
||||
<img
|
||||
src="https://cdn.affine.pro/mail/2023-8-9/Twitter.png"
|
||||
alt="AFFiNE twitter link"
|
||||
height="16px"
|
||||
/>
|
||||
</a>
|
||||
</td>
|
||||
<td style="padding: 0 10px">
|
||||
<a href="https://discord.gg/Arn7TqJBvG" target="_blank"
|
||||
><img
|
||||
src="https://cdn.affine.pro/mail/2023-8-9/Discord.png"
|
||||
alt="AFFiNE discord link"
|
||||
height="16px"
|
||||
/></a>
|
||||
</td>
|
||||
<td style="padding: 0 10px">
|
||||
<a href="https://www.youtube.com/@affinepro" target="_blank"
|
||||
><img
|
||||
src="https://cdn.affine.pro/mail/2023-8-9/Youtube.png"
|
||||
alt="AFFiNE youtube link"
|
||||
height="16px"
|
||||
/></a>
|
||||
</td>
|
||||
<td style="padding: 0 10px">
|
||||
<a href="https://t.me/affineworkos" target="_blank"
|
||||
><img
|
||||
src="https://cdn.affine.pro/mail/2023-8-9/Telegram.png"
|
||||
alt="AFFiNE telegram link"
|
||||
height="16px"
|
||||
/></a>
|
||||
</td>
|
||||
<td style="padding: 0 10px">
|
||||
<a href="https://www.reddit.com/r/Affine/" target="_blank"
|
||||
><img
|
||||
src="https://cdn.affine.pro/mail/2023-8-9/Reddit.png"
|
||||
alt="AFFiNE reddit link"
|
||||
height="16px"
|
||||
/></a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr align="center">
|
||||
<td
|
||||
style="
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
font-family: inter, Arial, Helvetica, sans-serif;
|
||||
color: #8e8d91;
|
||||
padding-top: 8px;
|
||||
"
|
||||
>
|
||||
One hyper-fused platform for wildly creative minds
|
||||
</td>
|
||||
</tr>
|
||||
<tr align="center">
|
||||
<td
|
||||
style="
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
font-family: inter, Arial, Helvetica, sans-serif;
|
||||
color: #8e8d91;
|
||||
padding-top: 8px;
|
||||
"
|
||||
>
|
||||
Copyright<img
|
||||
src="https://cdn.affine.pro/mail/2023-8-9/copyright.png"
|
||||
alt="copyright"
|
||||
height="14px"
|
||||
style="vertical-align: middle; margin: 0 4px"
|
||||
/>2023 Toeverything
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>`;
|
||||
};
|
||||
Reference in New Issue
Block a user