feat(server): allow customize mailer server (#5835)

This commit is contained in:
liuyi 2024-02-19 14:37:08 +00:00
parent 3881164854
commit df157819dc
No known key found for this signature in database
GPG Key ID: 56709255DC7EC728
17 changed files with 110 additions and 134 deletions

View File

@ -15,9 +15,9 @@ const {
R2_SECRET_ACCESS_KEY,
ENABLE_CAPTCHA,
CAPTCHA_TURNSTILE_SECRET,
OAUTH_EMAIL_SENDER,
OAUTH_EMAIL_LOGIN,
OAUTH_EMAIL_PASSWORD,
MAILER_SENDER,
MAILER_USER,
MAILER_PASSWORD,
AFFINE_GOOGLE_CLIENT_ID,
AFFINE_GOOGLE_CLIENT_SECRET,
CLOUD_SQL_IAM_ACCOUNT,
@ -103,9 +103,9 @@ const createHelmCommand = ({ isDryRun }) => {
`--set-string graphql.app.objectStorage.r2.accountId="${R2_ACCOUNT_ID}"`,
`--set-string graphql.app.objectStorage.r2.accessKeyId="${R2_ACCESS_KEY_ID}"`,
`--set-string graphql.app.objectStorage.r2.secretAccessKey="${R2_SECRET_ACCESS_KEY}"`,
`--set-string graphql.app.oauth.email.sender="${OAUTH_EMAIL_SENDER}"`,
`--set-string graphql.app.oauth.email.login="${OAUTH_EMAIL_LOGIN}"`,
`--set-string graphql.app.oauth.email.password="${OAUTH_EMAIL_PASSWORD}"`,
`--set-string graphql.app.mailer.sender="${MAILER_SENDER}"`,
`--set-string graphql.app.mailer.user="${MAILER_USER}"`,
`--set-string graphql.app.mailer.password="${MAILER_PASSWORD}"`,
`--set-string graphql.app.oauth.google.enabled=true`,
`--set-string graphql.app.oauth.google.clientId="${AFFINE_GOOGLE_CLIENT_ID}"`,
`--set-string graphql.app.oauth.google.clientSecret="${AFFINE_GOOGLE_CLIENT_SECRET}"`,

View File

@ -85,31 +85,31 @@ spec:
value: "{{ .Values.app.features.earlyAccessPreview }}"
- name: FEATURES_SYNC_CLIENT_VERSION_CHECK
value: "{{ .Values.app.features.syncClientVersionCheck }}"
- name: OAUTH_EMAIL_SENDER
- name: MAILER_HOST
valueFrom:
secretKeyRef:
name: "{{ .Values.app.oauth.email.secretName }}"
key: sender
- name: OAUTH_EMAIL_LOGIN
name: "{{ .Values.app.mailer.secretName }}"
key: host
- name: MAILER_PORT
valueFrom:
secretKeyRef:
name: "{{ .Values.app.oauth.email.secretName }}"
key: login
- name: OAUTH_EMAIL_SERVER
valueFrom:
secretKeyRef:
name: "{{ .Values.app.oauth.email.secretName }}"
key: server
- name: OAUTH_EMAIL_PORT
valueFrom:
secretKeyRef:
name: "{{ .Values.app.oauth.email.secretName }}"
name: "{{ .Values.app.mailer.secretName }}"
key: port
- name: OAUTH_EMAIL_PASSWORD
- name: MAILER_USER
valueFrom:
secretKeyRef:
name: "{{ .Values.app.oauth.email.secretName }}"
name: "{{ .Values.app.mailer.secretName }}"
key: user
- name: MAILER_PASSWORD
valueFrom:
secretKeyRef:
name: "{{ .Values.app.mailer.secretName }}"
key: password
- name: MAILER_SENDER
valueFrom:
secretKeyRef:
name: "{{ .Values.app.mailer.secretName }}"
key: sender
- name: STRIPE_API_KEY
valueFrom:
secretKeyRef:

View File

@ -35,14 +35,7 @@ app:
accountId: ''
accessKeyId: ''
secretAccessKey: ''
oauth:
email:
secretName: 'oauth-email'
sender: 'noreply@toeverything.info'
login: ''
password: ''
server: 'smtp.gmail.com'
port: '465'
oauth:
google:
enabled: false
secretName: oauth-google
@ -53,6 +46,13 @@ app:
secretName: oauth-github
clientId: ''
clientSecret: ''
mailer:
secretName: 'mailer'
host: 'smtp.gmail.com'
port: '465'
user: ''
password: ''
sender: 'noreply@toeverything.info'
payment:
stripe:
secretName: 'stripe'

View File

@ -448,7 +448,6 @@ jobs:
${{ matrix.tests.script }}
env:
DEV_SERVER_URL: http://localhost:8080
ENABLE_LOCAL_EMAIL: true
- name: Upload test results
if: ${{ failure() }}

View File

@ -275,9 +275,9 @@ jobs:
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
ENABLE_CAPTCHA: true
CAPTCHA_TURNSTILE_SECRET: ${{ secrets.CAPTCHA_TURNSTILE_SECRET }}
OAUTH_EMAIL_SENDER: ${{ secrets.OAUTH_EMAIL_SENDER }}
OAUTH_EMAIL_LOGIN: ${{ secrets.OAUTH_EMAIL_LOGIN }}
OAUTH_EMAIL_PASSWORD: ${{ secrets.OAUTH_EMAIL_PASSWORD }}
MAILER_SENDER: ${{ secrets.OAUTH_EMAIL_SENDER }}
MAILER_USER: ${{ secrets.OAUTH_EMAIL_LOGIN }}
MAILER_PASSWORD: ${{ secrets.OAUTH_EMAIL_PASSWORD }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
AFFINE_GOOGLE_CLIENT_ID: ${{ secrets.AFFINE_GOOGLE_CLIENT_ID }}
AFFINE_GOOGLE_CLIENT_SECRET: ${{ secrets.AFFINE_GOOGLE_CLIENT_SECRET }}

View File

@ -59,9 +59,9 @@ You may need additional env for auth login. You may want to put your own one if
For email login & password, please refer to https://nodemailer.com/usage/using-gmail/
```
OAUTH_EMAIL_SENDER=
OAUTH_EMAIL_LOGIN=
OAUTH_EMAIL_PASSWORD=
MAILER_SENDER=
MAILER_USER=
MAILER_PASSWORD=
OAUTH_GOOGLE_ENABLED="true"
OAUTH_GOOGLE_CLIENT_ID=
OAUTH_GOOGLE_CLIENT_SECRET=

View File

@ -139,10 +139,11 @@
"environmentVariables": {
"TS_NODE_PROJECT": "./tests/tsconfig.json",
"NODE_ENV": "test",
"ENABLE_LOCAL_EMAIL": "true",
"OAUTH_EMAIL_LOGIN": "noreply@toeverything.info",
"OAUTH_EMAIL_PASSWORD": "affine",
"OAUTH_EMAIL_SENDER": "noreply@toeverything.info",
"MAILER_HOST": "0.0.0.0",
"MAILER_PORT": "1025",
"MAILER_USER": "noreply@toeverything.info",
"MAILER_PASSWORD": "affine",
"MAILER_SENDER": "noreply@toeverything.info",
"FEATURES_EARLY_ACCESS_PREVIEW": "false"
}
},

View File

@ -13,11 +13,12 @@ AFFiNE.ENV_MAP = {
OAUTH_GITHUB_ENABLED: ['auth.oauthProviders.github.enabled', 'boolean'],
OAUTH_GITHUB_CLIENT_ID: 'auth.oauthProviders.github.clientId',
OAUTH_GITHUB_CLIENT_SECRET: 'auth.oauthProviders.github.clientSecret',
OAUTH_EMAIL_LOGIN: 'auth.email.login',
OAUTH_EMAIL_SENDER: 'auth.email.sender',
OAUTH_EMAIL_SERVER: 'auth.email.server',
OAUTH_EMAIL_PORT: ['auth.email.port', 'int'],
OAUTH_EMAIL_PASSWORD: 'auth.email.password',
MAILER_HOST: 'mailer.host',
MAILER_PORT: ['mailer.port', 'int'],
MAILER_USER: 'mailer.auth.user',
MAILER_PASSWORD: 'mailer.auth.pass',
MAILER_SENDER: 'mailer.from.address',
MAILER_SECURE: ['mailer.secure', 'boolean'],
THROTTLE_TTL: ['rateLimiter.ttl', 'int'],
THROTTLE_LIMIT: ['rateLimiter.limit', 'int'],
REDIS_SERVER_HOST: 'plugins.redis.host',
@ -30,7 +31,6 @@ AFFiNE.ENV_MAP = {
'doc.manager.experimentalMergeWithYOcto',
'boolean',
],
ENABLE_LOCAL_EMAIL: ['auth.localEmail', 'boolean'],
STRIPE_API_KEY: 'plugins.payment.stripe.keys.APIKey',
STRIPE_WEBHOOK_KEY: 'plugins.payment.stripe.keys.webhookKey',
FEATURES_EARLY_ACCESS_PREVIEW: ['featureFlags.earlyAccessPreview', 'boolean'],

View File

@ -42,5 +42,13 @@ AFFiNE.plugins.use('redis');
AFFiNE.plugins.use('payment');
if (AFFiNE.deploy) {
AFFiNE.mailer = {
service: 'gmail',
auth: {
user: env.MAILER_USER,
pass: env.MAILER_PASSWORD,
},
};
AFFiNE.plugins.use('gcloud');
}

View File

@ -97,22 +97,7 @@ export const NextAuthOptionsProvider: FactoryProvider<NextAuthOptions> = {
return result;
};
const nextAuthOptions: NextAuthOptions = {
providers: [
// @ts-expect-error esm interop issue
Email.default({
server: {
host: config.auth.email.server,
port: config.auth.email.port,
auth: {
user: config.auth.email.login,
pass: config.auth.email.password,
},
},
from: config.auth.email.sender,
sendVerificationRequest: (params: SendVerificationRequestParams) =>
sendVerificationRequest(config, logger, mailer, session, params),
}),
],
providers: [],
adapter: prismaAdapter,
debug: !config.node.prod,
session: {
@ -138,6 +123,18 @@ export const NextAuthOptionsProvider: FactoryProvider<NextAuthOptions> = {
},
};
if (config.mailer && mailer) {
nextAuthOptions.providers.push(
// @ts-expect-error esm interop issue
Email.default({
server: config.mailer,
from: config.mailer.from,
sendVerificationRequest: (params: SendVerificationRequestParams) =>
sendVerificationRequest(config, logger, mailer, session, params),
})
);
}
nextAuthOptions.providers.push(
// @ts-expect-error esm interop issue
Credentials.default({

View File

@ -1,4 +1,5 @@
import type { ApolloDriverConfig } from '@nestjs/apollo';
import SMTPTransport from 'nodemailer/lib/smtp-transport';
import type { LeafPaths } from '../utils/types';
import { EnvConfigType } from './env';
@ -264,18 +265,6 @@ export interface AFFiNEConfig {
}
>
>;
/**
* whether to use local email service to send email
* local debug only
*/
localEmail: boolean;
email: {
server: string;
port: number;
login: string;
sender: string;
password: string;
};
captcha: {
/**
* whether to enable captcha
@ -299,6 +288,13 @@ export interface AFFiNEConfig {
};
};
/**
* Configurations for mail service used to post auth or bussiness mails.
*
* @see https://nodemailer.com/smtp/
*/
mailer?: SMTPTransport.Options;
doc: {
manager: {
/**

View File

@ -167,14 +167,6 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
return this.privateKey;
},
oauthProviders: {},
localEmail: false,
email: {
server: 'smtp.gmail.com',
port: 465,
login: '',
sender: '',
password: '',
},
},
storage: getDefaultAFFiNEStorageConfig(),
rateLimiter: {

View File

@ -1,11 +1,21 @@
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.auth.user', 'mailer.auth.pass'],
})
class MailerModule {}
@Global()
@Module({
providers: [MAILER, MailService],
imports: [MailerModule],
providers: [MailService],
exports: [MailService],
})
export class MailModule {}

View File

@ -1,30 +1,28 @@
import { Inject, Injectable } from '@nestjs/common';
import { Inject, Injectable, Optional } from '@nestjs/common';
import { Config } from '../config';
import {
MAILER_SERVICE,
type MailerService,
type Options,
type Response,
} from './mailer';
import { MAILER_SERVICE, type MailerService, type Options } from './mailer';
import { emailTemplate } from './template';
@Injectable()
export class MailService {
constructor(
@Inject(MAILER_SERVICE) private readonly mailer: MailerService,
private readonly config: Config
private readonly config: Config,
@Optional() @Inject(MAILER_SERVICE) private readonly mailer?: MailerService
) {}
async sendMail(options: Options): Promise<Response> {
return this.mailer.sendMail(options);
async sendMail(options: Options) {
if (!this.mailer) {
throw new Error('Mailer service is not configured.');
}
return this.mailer.sendMail({
from: this.config.mailer?.from,
...options,
});
}
hasConfigured() {
return (
!!this.config.auth.email.login &&
!!this.config.auth.email.password &&
!!this.config.auth.email.sender
);
return !!this.mailer;
}
async sendInviteEmail(
@ -80,7 +78,6 @@ export class MailService {
});
return this.sendMail({
from: this.config.auth.email.sender,
to,
subject: `${invitationInfo.user.name} invited you to join ${invitationInfo.workspace.name}`,
html,
@ -119,7 +116,6 @@ export class MailService {
buttonUrl: url,
});
return this.sendMail({
from: this.config.auth.email.sender,
to,
subject: `Modify your AFFiNE password`,
html,
@ -135,7 +131,6 @@ export class MailService {
buttonUrl: url,
});
return this.sendMail({
from: this.config.auth.email.sender,
to,
subject: `Set your AFFiNE password`,
html,
@ -150,7 +145,6 @@ export class MailService {
buttonUrl: url,
});
return this.sendMail({
from: this.config.auth.email.sender,
to,
subject: `Verify your current email for AFFiNE`,
html,
@ -165,7 +159,6 @@ export class MailService {
buttonUrl: url,
});
return this.sendMail({
from: this.config.auth.email.sender,
to,
subject: `Verify your new email for AFFiNE`,
html,
@ -177,7 +170,6 @@ export class MailService {
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,
@ -200,7 +192,6 @@ export class MailService {
content: `${inviteeName} has joined ${workspaceName}`,
});
return this.sendMail({
from: this.config.auth.email.sender,
to,
subject: title,
html,
@ -223,7 +214,6 @@ export class MailService {
content: `${inviteeName} has left your workspace`,
});
return this.sendMail({
from: this.config.auth.email.sender,
to,
subject: title,
html,

View File

@ -11,28 +11,11 @@ export type Response = SMTPTransport.SentMessageInfo;
export type Options = SMTPTransport.Options;
export const MAILER: FactoryProvider<
Transporter<SMTPTransport.SentMessageInfo>
Transporter<SMTPTransport.SentMessageInfo> | undefined
> = {
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,
},
});
return config.mailer ? createTransport(config.mailer) : undefined;
},
inject: [Config],
};

View File

@ -52,11 +52,12 @@ const config: PlaywrightTestConfig = {
DEBUG: 'affine:*',
FORCE_COLOR: 'true',
DEBUG_COLORS: 'true',
ENABLE_LOCAL_EMAIL: process.env.ENABLE_LOCAL_EMAIL ?? 'true',
NEXTAUTH_URL: 'http://localhost:8080',
OAUTH_EMAIL_SENDER: 'noreply@toeverything.info',
OAUTH_EMAIL_LOGIN: 'noreply@toeverything.info',
OAUTH_EMAIL_PASSWORD: 'affine',
MAILER_HOST: '0.0.0.0',
MAILER_PORT: '1025',
MAILER_SENDER: 'noreply@toeverything.info',
MAILER_USER: 'noreply@toeverything.info',
MAILER_PASSWORD: 'affine',
},
},
],

View File

@ -47,9 +47,8 @@ const config: PlaywrightTestConfig = {
DEBUG: 'affine:*',
FORCE_COLOR: 'true',
DEBUG_COLORS: 'true',
ENABLE_LOCAL_EMAIL: process.env.ENABLE_LOCAL_EMAIL ?? 'true',
NEXTAUTH_URL: 'http://localhost:8080',
OAUTH_EMAIL_SENDER: 'noreply@toeverything.info',
MAILER_SENDER: 'noreply@toeverything.info',
},
},
],