diff --git a/.docker/selfhost/schema.json b/.docker/selfhost/schema.json index b66928626e..579dad67f0 100644 --- a/.docker/selfhost/schema.json +++ b/.docker/selfhost/schema.json @@ -200,11 +200,6 @@ "type": "object", "description": "Configuration for mailer module", "properties": { - "enabled": { - "type": "boolean", - "description": "Whether enabled mail service.\n@default false", - "default": false - }, "SMTP.host": { "type": "string", "description": "Host of the email server (e.g. smtp.gmail.com)\n@default \"\"\n@environment `MAILER_HOST`", diff --git a/packages/backend/server/src/base/error/def.ts b/packages/backend/server/src/base/error/def.ts index 94783b6076..7f520ccb91 100644 --- a/packages/backend/server/src/base/error/def.ts +++ b/packages/backend/server/src/base/error/def.ts @@ -275,6 +275,10 @@ export const USER_FRIENDLY_ERRORS = { args: { message: 'string' }, message: ({ message }) => `HTTP request error, message: ${message}`, }, + email_service_not_configured: { + type: 'internal_server_error', + message: 'Email service is not configured.', + }, // Input errors query_too_long: { diff --git a/packages/backend/server/src/base/error/errors.gen.ts b/packages/backend/server/src/base/error/errors.gen.ts index d02b0d72b3..0a6a4bdbb0 100644 --- a/packages/backend/server/src/base/error/errors.gen.ts +++ b/packages/backend/server/src/base/error/errors.gen.ts @@ -54,6 +54,12 @@ export class HttpRequestError extends UserFriendlyError { super('bad_request', 'http_request_error', message, args); } } + +export class EmailServiceNotConfigured extends UserFriendlyError { + constructor(message?: string) { + super('internal_server_error', 'email_service_not_configured', message); + } +} @ObjectType() class QueryTooLongDataType { @Field() max!: number @@ -943,6 +949,7 @@ export enum ErrorNames { BAD_REQUEST, GRAPHQL_BAD_REQUEST, HTTP_REQUEST_ERROR, + EMAIL_SERVICE_NOT_CONFIGURED, QUERY_TOO_LONG, VALIDATION_ERROR, USER_NOT_FOUND, diff --git a/packages/backend/server/src/core/auth/controller.ts b/packages/backend/server/src/core/auth/controller.ts index 659c7ccbf0..d77c615884 100644 --- a/packages/backend/server/src/core/auth/controller.ts +++ b/packages/backend/server/src/core/auth/controller.ts @@ -37,7 +37,6 @@ import { CurrentUser, Session } from './session'; interface PreflightResponse { registered: boolean; hasPassword: boolean; - magicLink: boolean; } interface SignInCredential { @@ -91,20 +90,16 @@ export class AuthController { const user = await this.models.user.getUserByEmail(params.email); - const magicLinkAvailable = this.config.mailer.enabled; - if (!user) { return { registered: false, hasPassword: false, - magicLink: magicLinkAvailable, }; } return { registered: user.registered, hasPassword: !!user.password, - magicLink: magicLinkAvailable, }; } diff --git a/packages/backend/server/src/core/mail/config.ts b/packages/backend/server/src/core/mail/config.ts index a77dd28c52..ee63102ac4 100644 --- a/packages/backend/server/src/core/mail/config.ts +++ b/packages/backend/server/src/core/mail/config.ts @@ -3,7 +3,6 @@ import { defineModuleConfig } from '../../base'; declare global { interface AppConfigSchema { mailer: { - enabled: boolean; SMTP: { host: string; port: number; @@ -17,10 +16,6 @@ declare global { } defineModuleConfig('mailer', { - enabled: { - desc: 'Whether enabled mail service.', - default: false, - }, 'SMTP.host': { desc: 'Host of the email server (e.g. smtp.gmail.com)', default: '', @@ -49,6 +44,6 @@ defineModuleConfig('mailer', { 'SMTP.ignoreTLS': { desc: "Whether ignore email server's TSL certification verification. Enable it for self-signed certificates.", default: false, - env: 'MAILER_IGNORE_TLS', + env: ['MAILER_IGNORE_TLS', 'boolean'], }, }); diff --git a/packages/backend/server/src/core/mail/mailer.ts b/packages/backend/server/src/core/mail/mailer.ts index 3263ee1b24..4abef4b0e5 100644 --- a/packages/backend/server/src/core/mail/mailer.ts +++ b/packages/backend/server/src/core/mail/mailer.ts @@ -1,12 +1,25 @@ import { Injectable } from '@nestjs/common'; -import { JobQueue } from '../../base'; +import { EmailServiceNotConfigured, JobQueue } from '../../base'; +import { MailSender } from './sender'; @Injectable() export class Mailer { - constructor(private readonly queue: JobQueue) {} + constructor( + private readonly queue: JobQueue, + private readonly sender: MailSender + ) {} + + get enabled() { + // @ts-expect-error internal api + return this.sender.smtp !== null; + } async send(command: Jobs['notification.sendMail']) { + if (!this.enabled) { + throw new EmailServiceNotConfigured(); + } + try { await this.queue.add('notification.sendMail', command); return true; diff --git a/packages/backend/server/src/core/mail/sender.ts b/packages/backend/server/src/core/mail/sender.ts index fbce9584a8..1304fb0f75 100644 --- a/packages/backend/server/src/core/mail/sender.ts +++ b/packages/backend/server/src/core/mail/sender.ts @@ -56,13 +56,7 @@ export class MailSender { } private setup() { - const { SMTP, enabled } = this.config.mailer; - - if (!enabled) { - this.smtp = null; - return; - } - + const { SMTP } = this.config.mailer; const opts = configToSMTPOptions(SMTP); if (SMTP.host) { @@ -83,6 +77,7 @@ export class MailSender { }); } else { this.logger.warn('Mailer SMTP transport is not configured.'); + this.smtp = null; } } diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql index a69e6e2d8d..1067e1dbe1 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -447,6 +447,7 @@ enum ErrorNames { DOC_UPDATE_BLOCKED EARLY_ACCESS_REQUIRED EMAIL_ALREADY_USED + EMAIL_SERVICE_NOT_CONFIGURED EMAIL_TOKEN_NOT_FOUND EMAIL_VERIFICATION_REQUIRED EXPECT_TO_GRANT_DOC_USER_ROLES diff --git a/packages/common/graphql/src/schema.ts b/packages/common/graphql/src/schema.ts index e8167a5fef..7ce4d43b21 100644 --- a/packages/common/graphql/src/schema.ts +++ b/packages/common/graphql/src/schema.ts @@ -592,6 +592,7 @@ export enum ErrorNames { DOC_UPDATE_BLOCKED = 'DOC_UPDATE_BLOCKED', EARLY_ACCESS_REQUIRED = 'EARLY_ACCESS_REQUIRED', EMAIL_ALREADY_USED = 'EMAIL_ALREADY_USED', + EMAIL_SERVICE_NOT_CONFIGURED = 'EMAIL_SERVICE_NOT_CONFIGURED', EMAIL_TOKEN_NOT_FOUND = 'EMAIL_TOKEN_NOT_FOUND', EMAIL_VERIFICATION_REQUIRED = 'EMAIL_VERIFICATION_REQUIRED', EXPECT_TO_GRANT_DOC_USER_ROLES = 'EXPECT_TO_GRANT_DOC_USER_ROLES', diff --git a/packages/frontend/admin/src/config.json b/packages/frontend/admin/src/config.json index c2be4bc642..df6c0224ef 100644 --- a/packages/frontend/admin/src/config.json +++ b/packages/frontend/admin/src/config.json @@ -92,10 +92,6 @@ } }, "mailer": { - "enabled": { - "type": "Boolean", - "desc": "Whether enabled mail service." - }, "SMTP.host": { "type": "String", "desc": "Host of the email server (e.g. smtp.gmail.com)", diff --git a/packages/frontend/admin/src/modules/settings/config.ts b/packages/frontend/admin/src/modules/settings/config.ts index c3710ab6a9..f33125467a 100644 --- a/packages/frontend/admin/src/modules/settings/config.ts +++ b/packages/frontend/admin/src/modules/settings/config.ts @@ -76,7 +76,6 @@ export const KNOWN_CONFIG_GROUPS = [ name: 'Notification', module: 'mailer', fields: [ - 'enabled', 'SMTP.host', 'SMTP.port', 'SMTP.username', diff --git a/packages/frontend/core/src/components/sign-in/sign-in-with-email.tsx b/packages/frontend/core/src/components/sign-in/sign-in-with-email.tsx index e1c0152e67..35ebce5873 100644 --- a/packages/frontend/core/src/components/sign-in/sign-in-with-email.tsx +++ b/packages/frontend/core/src/components/sign-in/sign-in-with-email.tsx @@ -11,7 +11,7 @@ import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hoo import { AuthService, CaptchaService } from '@affine/core/modules/cloud'; import type { AuthSessionStatus } from '@affine/core/modules/cloud/entities/session'; import { Unreachable } from '@affine/env/constant'; -import type { UserFriendlyError } from '@affine/error'; +import { UserFriendlyError } from '@affine/error'; import { Trans, useI18n } from '@affine/i18n'; import { useLiveData, useService } from '@toeverything/infra'; import { @@ -95,8 +95,10 @@ export const SignInWithEmailStep = ({ ); } catch (err) { console.error(err); + const error = UserFriendlyError.fromAny(err); notify.error({ - title: 'Failed to send email, please try again.', + title: 'Failed to sign in', + message: t[`error.${error.name}`](error.data), }); } setIsSending(false); @@ -109,6 +111,7 @@ export const SignInWithEmailStep = ({ needCaptcha, state.redirectUrl, verifyToken, + t, ]); useEffect(() => { diff --git a/packages/frontend/core/src/components/sign-in/sign-in.tsx b/packages/frontend/core/src/components/sign-in/sign-in.tsx index f10161229f..250d7fc12f 100644 --- a/packages/frontend/core/src/components/sign-in/sign-in.tsx +++ b/packages/frontend/core/src/components/sign-in/sign-in.tsx @@ -93,46 +93,22 @@ export const SignInStep = ({ setIsMutating(true); try { - const { hasPassword, registered, magicLink } = - await authService.checkUserByEmail(email); + const { hasPassword } = await authService.checkUserByEmail(email); - if (registered) { - // provider password sign-in if user has by default - // If with payment, onl support email sign in to avoid redirect to affine app - if (hasPassword) { - changeState(prev => ({ - ...prev, - email, - step: 'signInWithPassword', - hasPassword: true, - })); - } else { - if (magicLink) { - changeState(prev => ({ - ...prev, - email, - step: 'signInWithEmail', - hasPassword: false, - })); - } else { - notify.error({ - title: 'Failed to send email. Please contact the administrator.', - }); - } - } + if (hasPassword) { + changeState(prev => ({ + ...prev, + email, + step: 'signInWithPassword', + hasPassword: true, + })); } else { - if (magicLink) { - changeState(prev => ({ - ...prev, - email, - step: 'signInWithEmail', - hasPassword: false, - })); - } else { - notify.error({ - title: 'Failed to send email. Please contact the administrator.', - }); - } + changeState(prev => ({ + ...prev, + email, + step: 'signInWithEmail', + hasPassword: false, + })); } } catch (err: any) { console.error(err); diff --git a/packages/frontend/i18n/src/i18n.gen.ts b/packages/frontend/i18n/src/i18n.gen.ts index 9e5ab5ebec..fd203ded45 100644 --- a/packages/frontend/i18n/src/i18n.gen.ts +++ b/packages/frontend/i18n/src/i18n.gen.ts @@ -7690,6 +7690,10 @@ export function useAFFiNEI18N(): { ["error.HTTP_REQUEST_ERROR"](options: { readonly message: string; }): string; + /** + * `Email service is not configured.` + */ + ["error.EMAIL_SERVICE_NOT_CONFIGURED"](): string; /** * `Query is too long, max length is {{max}}.` */ diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index c643c0971e..11a20a804d 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1923,6 +1923,7 @@ "error.BAD_REQUEST": "Bad request.", "error.GRAPHQL_BAD_REQUEST": "GraphQL bad request, code: {{code}}, {{message}}", "error.HTTP_REQUEST_ERROR": "HTTP request error, message: {{message}}", + "error.EMAIL_SERVICE_NOT_CONFIGURED": "Email service is not configured.", "error.QUERY_TOO_LONG": "Query is too long, max length is {{max}}.", "error.VALIDATION_ERROR": "Validation error, errors: {{errors}}", "error.USER_NOT_FOUND": "User not found.",