From df157819dc3a6246db0441157132aabe937be9fa Mon Sep 17 00:00:00 2001 From: liuyi Date: Mon, 19 Feb 2024 14:37:08 +0000 Subject: [PATCH] feat(server): allow customize mailer server (#5835) --- .github/actions/deploy/deploy.mjs | 12 +++--- .../charts/graphql/templates/deployment.yaml | 34 ++++++++--------- .../helm/affine/charts/graphql/values.yaml | 16 ++++---- .github/workflows/build-test.yml | 1 - .github/workflows/deploy.yml | 6 +-- docs/developing-server.md | 6 +-- packages/backend/server/package.json | 9 +++-- .../backend/server/src/config/affine.env.ts | 12 +++--- .../backend/server/src/config/affine.self.ts | 8 ++++ .../server/src/core/auth/next-auth-options.ts | 29 +++++++------- .../server/src/fundamentals/config/def.ts | 20 ++++------ .../server/src/fundamentals/config/default.ts | 8 ---- .../server/src/fundamentals/mailer/index.ts | 12 +++++- .../src/fundamentals/mailer/mail.service.ts | 38 +++++++------------ .../server/src/fundamentals/mailer/mailer.ts | 21 +--------- tests/affine-cloud/playwright.config.ts | 9 +++-- .../affine-desktop-cloud/playwright.config.ts | 3 +- 17 files changed, 110 insertions(+), 134 deletions(-) diff --git a/.github/actions/deploy/deploy.mjs b/.github/actions/deploy/deploy.mjs index d5297f378..c3e7c5865 100644 --- a/.github/actions/deploy/deploy.mjs +++ b/.github/actions/deploy/deploy.mjs @@ -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}"`, diff --git a/.github/helm/affine/charts/graphql/templates/deployment.yaml b/.github/helm/affine/charts/graphql/templates/deployment.yaml index 06d7bf77e..5cc2d61ba 100644 --- a/.github/helm/affine/charts/graphql/templates/deployment.yaml +++ b/.github/helm/affine/charts/graphql/templates/deployment.yaml @@ -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: diff --git a/.github/helm/affine/charts/graphql/values.yaml b/.github/helm/affine/charts/graphql/values.yaml index 78cd80e9c..a4f01e653 100644 --- a/.github/helm/affine/charts/graphql/values.yaml +++ b/.github/helm/affine/charts/graphql/values.yaml @@ -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' diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index df596e582..f0ffe2aaf 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -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() }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index bc598c818..c9d14b898 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -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 }} diff --git a/docs/developing-server.md b/docs/developing-server.md index 843fc2d4d..2c044f25b 100644 --- a/docs/developing-server.md +++ b/docs/developing-server.md @@ -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= diff --git a/packages/backend/server/package.json b/packages/backend/server/package.json index 4fefebd0e..69ec5e2bc 100644 --- a/packages/backend/server/package.json +++ b/packages/backend/server/package.json @@ -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" } }, diff --git a/packages/backend/server/src/config/affine.env.ts b/packages/backend/server/src/config/affine.env.ts index 0aae7ff90..8b4aadc04 100644 --- a/packages/backend/server/src/config/affine.env.ts +++ b/packages/backend/server/src/config/affine.env.ts @@ -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'], diff --git a/packages/backend/server/src/config/affine.self.ts b/packages/backend/server/src/config/affine.self.ts index 65d1b2756..96cf69c84 100644 --- a/packages/backend/server/src/config/affine.self.ts +++ b/packages/backend/server/src/config/affine.self.ts @@ -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'); } diff --git a/packages/backend/server/src/core/auth/next-auth-options.ts b/packages/backend/server/src/core/auth/next-auth-options.ts index e6308f3f6..3a80c2edd 100644 --- a/packages/backend/server/src/core/auth/next-auth-options.ts +++ b/packages/backend/server/src/core/auth/next-auth-options.ts @@ -97,22 +97,7 @@ export const NextAuthOptionsProvider: FactoryProvider = { 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 = { }, }; + 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({ diff --git a/packages/backend/server/src/fundamentals/config/def.ts b/packages/backend/server/src/fundamentals/config/def.ts index 833de588a..2dc04cad3 100644 --- a/packages/backend/server/src/fundamentals/config/def.ts +++ b/packages/backend/server/src/fundamentals/config/def.ts @@ -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: { /** diff --git a/packages/backend/server/src/fundamentals/config/default.ts b/packages/backend/server/src/fundamentals/config/default.ts index 49bc5a608..9c884a136 100644 --- a/packages/backend/server/src/fundamentals/config/default.ts +++ b/packages/backend/server/src/fundamentals/config/default.ts @@ -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: { diff --git a/packages/backend/server/src/fundamentals/mailer/index.ts b/packages/backend/server/src/fundamentals/mailer/index.ts index e990f6c48..b91a7a8fb 100644 --- a/packages/backend/server/src/fundamentals/mailer/index.ts +++ b/packages/backend/server/src/fundamentals/mailer/index.ts @@ -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 {} diff --git a/packages/backend/server/src/fundamentals/mailer/mail.service.ts b/packages/backend/server/src/fundamentals/mailer/mail.service.ts index 66241e90c..aab0620a4 100644 --- a/packages/backend/server/src/fundamentals/mailer/mail.service.ts +++ b/packages/backend/server/src/fundamentals/mailer/mail.service.ts @@ -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 { - 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, diff --git a/packages/backend/server/src/fundamentals/mailer/mailer.ts b/packages/backend/server/src/fundamentals/mailer/mailer.ts index ba02cc888..1f8affac2 100644 --- a/packages/backend/server/src/fundamentals/mailer/mailer.ts +++ b/packages/backend/server/src/fundamentals/mailer/mailer.ts @@ -11,28 +11,11 @@ export type Response = SMTPTransport.SentMessageInfo; export type Options = SMTPTransport.Options; export const MAILER: FactoryProvider< - Transporter + Transporter | 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], }; diff --git a/tests/affine-cloud/playwright.config.ts b/tests/affine-cloud/playwright.config.ts index 1587aef56..37666a5ad 100644 --- a/tests/affine-cloud/playwright.config.ts +++ b/tests/affine-cloud/playwright.config.ts @@ -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', }, }, ], diff --git a/tests/affine-desktop-cloud/playwright.config.ts b/tests/affine-desktop-cloud/playwright.config.ts index b732df17b..4016026f5 100644 --- a/tests/affine-desktop-cloud/playwright.config.ts +++ b/tests/affine-desktop-cloud/playwright.config.ts @@ -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', }, }, ],