@@ -200,11 +200,6 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "Configuration for mailer module",
|
"description": "Configuration for mailer module",
|
||||||
"properties": {
|
"properties": {
|
||||||
"enabled": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Whether enabled mail service.\n@default false",
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"SMTP.host": {
|
"SMTP.host": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Host of the email server (e.g. smtp.gmail.com)\n@default \"\"\n@environment `MAILER_HOST`",
|
"description": "Host of the email server (e.g. smtp.gmail.com)\n@default \"\"\n@environment `MAILER_HOST`",
|
||||||
|
|||||||
@@ -275,6 +275,10 @@ export const USER_FRIENDLY_ERRORS = {
|
|||||||
args: { message: 'string' },
|
args: { message: 'string' },
|
||||||
message: ({ message }) => `HTTP request error, message: ${message}`,
|
message: ({ message }) => `HTTP request error, message: ${message}`,
|
||||||
},
|
},
|
||||||
|
email_service_not_configured: {
|
||||||
|
type: 'internal_server_error',
|
||||||
|
message: 'Email service is not configured.',
|
||||||
|
},
|
||||||
|
|
||||||
// Input errors
|
// Input errors
|
||||||
query_too_long: {
|
query_too_long: {
|
||||||
|
|||||||
@@ -54,6 +54,12 @@ export class HttpRequestError extends UserFriendlyError {
|
|||||||
super('bad_request', 'http_request_error', message, args);
|
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()
|
@ObjectType()
|
||||||
class QueryTooLongDataType {
|
class QueryTooLongDataType {
|
||||||
@Field() max!: number
|
@Field() max!: number
|
||||||
@@ -943,6 +949,7 @@ export enum ErrorNames {
|
|||||||
BAD_REQUEST,
|
BAD_REQUEST,
|
||||||
GRAPHQL_BAD_REQUEST,
|
GRAPHQL_BAD_REQUEST,
|
||||||
HTTP_REQUEST_ERROR,
|
HTTP_REQUEST_ERROR,
|
||||||
|
EMAIL_SERVICE_NOT_CONFIGURED,
|
||||||
QUERY_TOO_LONG,
|
QUERY_TOO_LONG,
|
||||||
VALIDATION_ERROR,
|
VALIDATION_ERROR,
|
||||||
USER_NOT_FOUND,
|
USER_NOT_FOUND,
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ import { CurrentUser, Session } from './session';
|
|||||||
interface PreflightResponse {
|
interface PreflightResponse {
|
||||||
registered: boolean;
|
registered: boolean;
|
||||||
hasPassword: boolean;
|
hasPassword: boolean;
|
||||||
magicLink: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SignInCredential {
|
interface SignInCredential {
|
||||||
@@ -91,20 +90,16 @@ export class AuthController {
|
|||||||
|
|
||||||
const user = await this.models.user.getUserByEmail(params.email);
|
const user = await this.models.user.getUserByEmail(params.email);
|
||||||
|
|
||||||
const magicLinkAvailable = this.config.mailer.enabled;
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
registered: false,
|
registered: false,
|
||||||
hasPassword: false,
|
hasPassword: false,
|
||||||
magicLink: magicLinkAvailable,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
registered: user.registered,
|
registered: user.registered,
|
||||||
hasPassword: !!user.password,
|
hasPassword: !!user.password,
|
||||||
magicLink: magicLinkAvailable,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { defineModuleConfig } from '../../base';
|
|||||||
declare global {
|
declare global {
|
||||||
interface AppConfigSchema {
|
interface AppConfigSchema {
|
||||||
mailer: {
|
mailer: {
|
||||||
enabled: boolean;
|
|
||||||
SMTP: {
|
SMTP: {
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
@@ -17,10 +16,6 @@ declare global {
|
|||||||
}
|
}
|
||||||
|
|
||||||
defineModuleConfig('mailer', {
|
defineModuleConfig('mailer', {
|
||||||
enabled: {
|
|
||||||
desc: 'Whether enabled mail service.',
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
'SMTP.host': {
|
'SMTP.host': {
|
||||||
desc: 'Host of the email server (e.g. smtp.gmail.com)',
|
desc: 'Host of the email server (e.g. smtp.gmail.com)',
|
||||||
default: '',
|
default: '',
|
||||||
@@ -49,6 +44,6 @@ defineModuleConfig('mailer', {
|
|||||||
'SMTP.ignoreTLS': {
|
'SMTP.ignoreTLS': {
|
||||||
desc: "Whether ignore email server's TSL certification verification. Enable it for self-signed certificates.",
|
desc: "Whether ignore email server's TSL certification verification. Enable it for self-signed certificates.",
|
||||||
default: false,
|
default: false,
|
||||||
env: 'MAILER_IGNORE_TLS',
|
env: ['MAILER_IGNORE_TLS', 'boolean'],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,25 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
import { JobQueue } from '../../base';
|
import { EmailServiceNotConfigured, JobQueue } from '../../base';
|
||||||
|
import { MailSender } from './sender';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class Mailer {
|
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']) {
|
async send(command: Jobs['notification.sendMail']) {
|
||||||
|
if (!this.enabled) {
|
||||||
|
throw new EmailServiceNotConfigured();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.queue.add('notification.sendMail', command);
|
await this.queue.add('notification.sendMail', command);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -56,13 +56,7 @@ export class MailSender {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private setup() {
|
private setup() {
|
||||||
const { SMTP, enabled } = this.config.mailer;
|
const { SMTP } = this.config.mailer;
|
||||||
|
|
||||||
if (!enabled) {
|
|
||||||
this.smtp = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const opts = configToSMTPOptions(SMTP);
|
const opts = configToSMTPOptions(SMTP);
|
||||||
|
|
||||||
if (SMTP.host) {
|
if (SMTP.host) {
|
||||||
@@ -83,6 +77,7 @@ export class MailSender {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.logger.warn('Mailer SMTP transport is not configured.');
|
this.logger.warn('Mailer SMTP transport is not configured.');
|
||||||
|
this.smtp = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -447,6 +447,7 @@ 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_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
|
||||||
|
|||||||
@@ -592,6 +592,7 @@ export enum ErrorNames {
|
|||||||
DOC_UPDATE_BLOCKED = 'DOC_UPDATE_BLOCKED',
|
DOC_UPDATE_BLOCKED = 'DOC_UPDATE_BLOCKED',
|
||||||
EARLY_ACCESS_REQUIRED = 'EARLY_ACCESS_REQUIRED',
|
EARLY_ACCESS_REQUIRED = 'EARLY_ACCESS_REQUIRED',
|
||||||
EMAIL_ALREADY_USED = 'EMAIL_ALREADY_USED',
|
EMAIL_ALREADY_USED = 'EMAIL_ALREADY_USED',
|
||||||
|
EMAIL_SERVICE_NOT_CONFIGURED = 'EMAIL_SERVICE_NOT_CONFIGURED',
|
||||||
EMAIL_TOKEN_NOT_FOUND = 'EMAIL_TOKEN_NOT_FOUND',
|
EMAIL_TOKEN_NOT_FOUND = 'EMAIL_TOKEN_NOT_FOUND',
|
||||||
EMAIL_VERIFICATION_REQUIRED = 'EMAIL_VERIFICATION_REQUIRED',
|
EMAIL_VERIFICATION_REQUIRED = 'EMAIL_VERIFICATION_REQUIRED',
|
||||||
EXPECT_TO_GRANT_DOC_USER_ROLES = 'EXPECT_TO_GRANT_DOC_USER_ROLES',
|
EXPECT_TO_GRANT_DOC_USER_ROLES = 'EXPECT_TO_GRANT_DOC_USER_ROLES',
|
||||||
|
|||||||
@@ -92,10 +92,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mailer": {
|
"mailer": {
|
||||||
"enabled": {
|
|
||||||
"type": "Boolean",
|
|
||||||
"desc": "Whether enabled mail service."
|
|
||||||
},
|
|
||||||
"SMTP.host": {
|
"SMTP.host": {
|
||||||
"type": "String",
|
"type": "String",
|
||||||
"desc": "Host of the email server (e.g. smtp.gmail.com)",
|
"desc": "Host of the email server (e.g. smtp.gmail.com)",
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ export const KNOWN_CONFIG_GROUPS = [
|
|||||||
name: 'Notification',
|
name: 'Notification',
|
||||||
module: 'mailer',
|
module: 'mailer',
|
||||||
fields: [
|
fields: [
|
||||||
'enabled',
|
|
||||||
'SMTP.host',
|
'SMTP.host',
|
||||||
'SMTP.port',
|
'SMTP.port',
|
||||||
'SMTP.username',
|
'SMTP.username',
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hoo
|
|||||||
import { AuthService, CaptchaService } from '@affine/core/modules/cloud';
|
import { AuthService, CaptchaService } from '@affine/core/modules/cloud';
|
||||||
import type { AuthSessionStatus } from '@affine/core/modules/cloud/entities/session';
|
import type { AuthSessionStatus } from '@affine/core/modules/cloud/entities/session';
|
||||||
import { Unreachable } from '@affine/env/constant';
|
import { Unreachable } from '@affine/env/constant';
|
||||||
import type { UserFriendlyError } from '@affine/error';
|
import { UserFriendlyError } from '@affine/error';
|
||||||
import { Trans, useI18n } from '@affine/i18n';
|
import { Trans, useI18n } from '@affine/i18n';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import {
|
import {
|
||||||
@@ -95,8 +95,10 @@ export const SignInWithEmailStep = ({
|
|||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
const error = UserFriendlyError.fromAny(err);
|
||||||
notify.error({
|
notify.error({
|
||||||
title: 'Failed to send email, please try again.',
|
title: 'Failed to sign in',
|
||||||
|
message: t[`error.${error.name}`](error.data),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setIsSending(false);
|
setIsSending(false);
|
||||||
@@ -109,6 +111,7 @@ export const SignInWithEmailStep = ({
|
|||||||
needCaptcha,
|
needCaptcha,
|
||||||
state.redirectUrl,
|
state.redirectUrl,
|
||||||
verifyToken,
|
verifyToken,
|
||||||
|
t,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -93,46 +93,22 @@ export const SignInStep = ({
|
|||||||
setIsMutating(true);
|
setIsMutating(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { hasPassword, registered, magicLink } =
|
const { hasPassword } = await authService.checkUserByEmail(email);
|
||||||
await authService.checkUserByEmail(email);
|
|
||||||
|
|
||||||
if (registered) {
|
if (hasPassword) {
|
||||||
// provider password sign-in if user has by default
|
changeState(prev => ({
|
||||||
// If with payment, onl support email sign in to avoid redirect to affine app
|
...prev,
|
||||||
if (hasPassword) {
|
email,
|
||||||
changeState(prev => ({
|
step: 'signInWithPassword',
|
||||||
...prev,
|
hasPassword: true,
|
||||||
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.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (magicLink) {
|
changeState(prev => ({
|
||||||
changeState(prev => ({
|
...prev,
|
||||||
...prev,
|
email,
|
||||||
email,
|
step: 'signInWithEmail',
|
||||||
step: 'signInWithEmail',
|
hasPassword: false,
|
||||||
hasPassword: false,
|
}));
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
notify.error({
|
|
||||||
title: 'Failed to send email. Please contact the administrator.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|||||||
@@ -7690,6 +7690,10 @@ export function useAFFiNEI18N(): {
|
|||||||
["error.HTTP_REQUEST_ERROR"](options: {
|
["error.HTTP_REQUEST_ERROR"](options: {
|
||||||
readonly message: string;
|
readonly message: string;
|
||||||
}): string;
|
}): string;
|
||||||
|
/**
|
||||||
|
* `Email service is not configured.`
|
||||||
|
*/
|
||||||
|
["error.EMAIL_SERVICE_NOT_CONFIGURED"](): string;
|
||||||
/**
|
/**
|
||||||
* `Query is too long, max length is {{max}}.`
|
* `Query is too long, max length is {{max}}.`
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1923,6 +1923,7 @@
|
|||||||
"error.BAD_REQUEST": "Bad request.",
|
"error.BAD_REQUEST": "Bad request.",
|
||||||
"error.GRAPHQL_BAD_REQUEST": "GraphQL bad request, code: {{code}}, {{message}}",
|
"error.GRAPHQL_BAD_REQUEST": "GraphQL bad request, code: {{code}}, {{message}}",
|
||||||
"error.HTTP_REQUEST_ERROR": "HTTP request error, message: {{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.QUERY_TOO_LONG": "Query is too long, max length is {{max}}.",
|
||||||
"error.VALIDATION_ERROR": "Validation error, errors: {{errors}}",
|
"error.VALIDATION_ERROR": "Validation error, errors: {{errors}}",
|
||||||
"error.USER_NOT_FOUND": "User not found.",
|
"error.USER_NOT_FOUND": "User not found.",
|
||||||
|
|||||||
Reference in New Issue
Block a user