chore: rename fundamentals to base (#9119)

This commit is contained in:
forehalo
2024-12-13 06:27:12 +00:00
parent 8c24f2b906
commit 4c23991047
185 changed files with 183 additions and 193 deletions
@@ -0,0 +1,16 @@
import SMTPTransport from 'nodemailer/lib/smtp-transport';
import { defineStartupConfig, ModuleConfig } from '../config';
declare module '../config' {
interface AppConfig {
/**
* Configurations for mail service used to post auth or bussiness mails.
*
* @see https://nodemailer.com/smtp/
*/
mailer: ModuleConfig<SMTPTransport.Options>;
}
}
defineStartupConfig('mailer', {});
@@ -0,0 +1,24 @@
import './config';
import { Global, Module } from '@nestjs/common';
import { OptionalModule } from '../nestjs';
import { MailService } from './mail.service';
import { MAILER } from './mailer';
@Global()
@OptionalModule({
providers: [MAILER],
exports: [MAILER],
requires: ['mailer.host'],
})
class MailerModule {}
@Global()
@Module({
imports: [MailerModule],
providers: [MailService],
exports: [MailService],
})
export class MailModule {}
export { MailService };
@@ -0,0 +1,314 @@
import { Inject, Injectable, Optional } from '@nestjs/common';
import { Config } from '../config';
import { MailerServiceIsNotConfigured } from '../error';
import { URLHelper } from '../helpers';
import { metrics } from '../metrics';
import type { MailerService, Options } from './mailer';
import { MAILER_SERVICE } from './mailer';
import { emailTemplate } from './template';
@Injectable()
export class MailService {
constructor(
private readonly config: Config,
private readonly url: URLHelper,
@Optional() @Inject(MAILER_SERVICE) private readonly mailer?: MailerService
) {}
async sendMail(options: Options) {
if (!this.mailer) {
throw new MailerServiceIsNotConfigured();
}
metrics.mail.counter('total').add(1);
try {
const result = await this.mailer.sendMail({
from: this.config.mailer?.from,
...options,
});
metrics.mail.counter('sent').add(1);
return result;
} catch (e) {
metrics.mail.counter('error').add(1);
throw e;
}
}
hasConfigured() {
return !!this.mailer;
}
async sendInviteEmail(
to: string,
inviteId: string,
invitationInfo: {
workspace: {
id: string;
name: string;
avatar: string;
};
user: {
avatar: string;
name: string;
};
}
) {
const buttonUrl = this.url.link(`/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 html = emailTemplate({
title: 'You are invited!',
content,
buttonContent: 'Accept & Join',
buttonUrl,
});
return this.sendMail({
to,
subject: `${invitationInfo.user.name} invited you to join ${invitationInfo.workspace.name}`,
html,
attachments: [
{
cid: 'workspaceAvatar',
filename: 'image.png',
content: workspaceAvatar,
encoding: 'base64',
},
],
});
}
async sendSignUpMail(url: string, options: Options) {
const html = emailTemplate({
title: 'Create AFFiNE Account',
content:
'Click the button below to complete your account creation and sign in. This magic link will expire in 30 minutes.',
buttonContent: ' Create account and sign in',
buttonUrl: url,
});
return this.sendMail({
html,
subject: 'Your AFFiNE account is waiting for you!',
...options,
});
}
async sendSignInMail(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({
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({
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({
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({
to,
subject: `Verify your new email for AFFiNE`,
html,
});
}
async sendVerifyEmail(to: string, url: string) {
const html = emailTemplate({
title: 'Verify your email address',
content:
'You recently requested to verify 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 email address',
buttonUrl: url,
});
return this.sendMail({
to,
subject: `Verify your 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({
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({
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({
to,
subject: title,
html,
});
}
// =================== Team Workspace Mails ===================
async sendReviewRequestMail(
to: string,
invitee: string,
ws: { id: string; name: string }
) {
const { id: workspaceId, name: workspaceName } = ws;
const title = `New request to join ${workspaceName}`;
const html = emailTemplate({
title: 'Request to join your workspace',
content: `${invitee} has requested to join ${workspaceName}. As a workspace owner/admin, you can approve or decline this request.`,
buttonContent: 'Review request',
buttonUrl: this.url.link(`/workspace/${workspaceId}`),
});
return this.sendMail({ to, subject: title, html });
}
async sendReviewApproveEmail(to: string, ws: { id: string; name: string }) {
const { id: workspaceId, name: workspaceName } = ws;
const title = `Your request to join ${workspaceName} has been approved`;
const html = emailTemplate({
title: 'Welcome to the workspace!',
content: `Your request to join ${workspaceName} has been accepted. You can now access the team workspace and collaborate with other members.`,
buttonContent: 'Open Workspace',
buttonUrl: this.url.link(`/workspace/${workspaceId}`),
});
return this.sendMail({ to, subject: title, html });
}
async sendReviewDeclinedEmail(to: string, ws: { name: string }) {
const { name: workspaceName } = ws;
const title = `Your request to join ${workspaceName} was declined`;
const html = emailTemplate({
title: 'Request declined',
content: `Your request to join ${workspaceName} has been declined by the workspace admin.`,
});
return this.sendMail({ to, subject: title, html });
}
}
@@ -0,0 +1,33 @@
import { FactoryProvider, Logger } 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> | undefined
> = {
provide: MAILER_SERVICE,
useFactory: (config: Config) => {
if (config.mailer) {
const logger = new Logger('Mailer');
const auth = config.mailer.auth;
if (auth && auth.user && !('pass' in auth)) {
logger.warn(
'Mailer service has not configured password, please make sure your mailer service allow empty password.'
);
}
return createTransport(config.mailer);
} else {
return undefined;
}
},
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/whd5mjYqVw" 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-${new Date().getUTCFullYear()} Toeverything
</td>
</tr>
</table>
</body>`;
};