mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 12:55:00 +00:00
feat(admin): adapt new config system (#11360)
feat(server): add test mail api feat(admin): adapt new config system
This commit is contained in:
@@ -6,6 +6,49 @@ Generated by [AVA](https://avajs.dev).
|
||||
|
||||
## should render emails
|
||||
|
||||
> Test Email from AFFiNE
|
||||
|
||||
`<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">␊
|
||||
<!--$-->␊
|
||||
<table␊
|
||||
align="center"␊
|
||||
width="100%"␊
|
||||
border="0"␊
|
||||
cellpadding="0"␊
|
||||
cellspacing="0"␊
|
||||
role="presentation">␊
|
||||
<tbody>␊
|
||||
<tr>␊
|
||||
<td>␊
|
||||
<p␊
|
||||
style="font-size:20px;line-height:28px;margin-bottom:0;margin-top:24px;font-weight:600;font-family:Inter, Arial, Helvetica, sans-serif;color:#141414">␊
|
||||
Test Email from AFFiNE␊
|
||||
</p>␊
|
||||
</td>␊
|
||||
</tr>␊
|
||||
</tbody>␊
|
||||
</table>␊
|
||||
<table␊
|
||||
align="center"␊
|
||||
width="100%"␊
|
||||
border="0"␊
|
||||
cellpadding="0"␊
|
||||
cellspacing="0"␊
|
||||
role="presentation">␊
|
||||
<tbody>␊
|
||||
<tr>␊
|
||||
<td>␊
|
||||
<p␊
|
||||
style="font-size:15px;line-height:24px;margin-bottom:0;margin-top:24px;font-weight:400;font-family:Inter, Arial, Helvetica, sans-serif;color:#141414">␊
|
||||
This is a test email from your AFFiNE instance.␊
|
||||
</p>␊
|
||||
</td>␊
|
||||
</tr>␊
|
||||
</tbody>␊
|
||||
</table>␊
|
||||
<!--/$-->␊
|
||||
`
|
||||
|
||||
> Sign in to AFFiNE
|
||||
|
||||
`<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">␊
|
||||
|
||||
Binary file not shown.
@@ -14,13 +14,8 @@ defineModuleConfig('graphql', {
|
||||
apolloDriverConfig: {
|
||||
desc: 'The config for underlying nestjs GraphQL and apollo driver engine.',
|
||||
default: {
|
||||
buildSchemaOptions: {
|
||||
numberScalarMode: 'integer',
|
||||
},
|
||||
useGlobalPrefix: true,
|
||||
playground: true,
|
||||
// @TODO(@forehalo): need a flag to tell user `Restart Required` configs
|
||||
introspection: true,
|
||||
sortSchema: true,
|
||||
},
|
||||
link: 'https://docs.nestjs.com/graphql/quick-start',
|
||||
},
|
||||
|
||||
@@ -26,6 +26,12 @@ export type GraphqlContext = {
|
||||
useFactory: (config: Config) => {
|
||||
return {
|
||||
...config.graphql.apolloDriverConfig,
|
||||
buildSchemaOptions: {
|
||||
numberScalarMode: 'integer',
|
||||
},
|
||||
useGlobalPrefix: true,
|
||||
playground: true,
|
||||
sortSchema: true,
|
||||
autoSchemaFile: join(
|
||||
env.projectRoot,
|
||||
env.testing
|
||||
|
||||
@@ -6,11 +6,12 @@ import { DocStorageModule } from '../doc';
|
||||
import { StorageModule } from '../storage';
|
||||
import { MailJob } from './job';
|
||||
import { Mailer } from './mailer';
|
||||
import { MailResolver } from './resolver';
|
||||
import { MailSender } from './sender';
|
||||
|
||||
@Module({
|
||||
imports: [DocStorageModule, StorageModule],
|
||||
providers: [MailSender, Mailer, MailJob],
|
||||
providers: [MailSender, Mailer, MailJob, MailResolver],
|
||||
exports: [Mailer],
|
||||
})
|
||||
export class MailModule {}
|
||||
|
||||
49
packages/backend/server/src/core/mail/resolver.ts
Normal file
49
packages/backend/server/src/core/mail/resolver.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Args, Mutation, Resolver } from '@nestjs/graphql';
|
||||
import { GraphQLJSONObject } from 'graphql-scalars';
|
||||
|
||||
import { BadRequest } from '../../base';
|
||||
import { Renderers } from '../../mails';
|
||||
import { CurrentUser } from '../auth/session';
|
||||
import { Admin } from '../common';
|
||||
import { MailSender } from './sender';
|
||||
|
||||
@Admin()
|
||||
@Resolver(() => Boolean)
|
||||
export class MailResolver {
|
||||
@Mutation(() => Boolean)
|
||||
async sendTestEmail(
|
||||
@CurrentUser() user: CurrentUser,
|
||||
@Args('config', { type: () => GraphQLJSONObject })
|
||||
config: AppConfig['mailer']['SMTP']
|
||||
) {
|
||||
const smtp = MailSender.create(config);
|
||||
|
||||
using _disposable = {
|
||||
[Symbol.dispose]: () => {
|
||||
smtp.close();
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
await smtp.verify();
|
||||
} catch (e) {
|
||||
throw new BadRequest(
|
||||
`Failed to verify your SMTP configuration. Cause: ${(e as Error).message}`
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await smtp.sendMail({
|
||||
from: config.sender,
|
||||
to: user.email,
|
||||
...(await Renderers.TestMail({})),
|
||||
});
|
||||
} catch (e) {
|
||||
throw new BadRequest(
|
||||
`Failed to send test email. Cause: ${(e as Error).message}`
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,22 @@ export type SendOptions = Omit<SendMailOptions, 'to' | 'subject' | 'html'> & {
|
||||
html: string;
|
||||
};
|
||||
|
||||
function configToSMTPOptions(
|
||||
config: AppConfig['mailer']['SMTP']
|
||||
): SMTPTransport.Options {
|
||||
return {
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
tls: {
|
||||
rejectUnauthorized: !config.ignoreTLS,
|
||||
},
|
||||
auth: {
|
||||
user: config.username,
|
||||
pass: config.password,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class MailSender {
|
||||
private readonly logger = new Logger(MailSender.name);
|
||||
@@ -23,6 +39,10 @@ export class MailSender {
|
||||
private usingTestAccount = false;
|
||||
constructor(private readonly config: Config) {}
|
||||
|
||||
static create(config: Config['mailer']['SMTP']) {
|
||||
return createTransport(configToSMTPOptions(config));
|
||||
}
|
||||
|
||||
@OnEvent('config.init')
|
||||
onConfigInit() {
|
||||
this.setup();
|
||||
@@ -43,17 +63,7 @@ export class MailSender {
|
||||
return;
|
||||
}
|
||||
|
||||
const opts: SMTPTransport.Options = {
|
||||
host: SMTP.host,
|
||||
port: SMTP.port,
|
||||
tls: {
|
||||
rejectUnauthorized: !SMTP.ignoreTLS,
|
||||
},
|
||||
auth: {
|
||||
user: SMTP.username,
|
||||
pass: SMTP.password,
|
||||
},
|
||||
};
|
||||
const opts = configToSMTPOptions(SMTP);
|
||||
|
||||
if (SMTP.host) {
|
||||
this.smtp = createTransport(opts);
|
||||
|
||||
@@ -195,7 +195,7 @@ export function Template(props: PropsWithChildren) {
|
||||
</>
|
||||
);
|
||||
|
||||
if (env.testing) {
|
||||
if (globalThis.env?.testing) {
|
||||
return content;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
TeamWorkspaceDeleted,
|
||||
TeamWorkspaceUpgraded,
|
||||
} from './teams';
|
||||
import TestMail from './test-mail';
|
||||
import {
|
||||
ChangeEmail,
|
||||
ChangeEmailNotification,
|
||||
@@ -45,7 +46,7 @@ function render(component: React.ReactElement) {
|
||||
});
|
||||
}
|
||||
|
||||
type Props<T> = T extends React.FC<infer P> ? P : never;
|
||||
type Props<T> = T extends React.ComponentType<infer P> ? P : never;
|
||||
export type EmailRenderer<Props> = (props: Props) => Promise<EmailContent>;
|
||||
|
||||
function make<T extends React.ComponentType<any>>(
|
||||
@@ -65,6 +66,10 @@ function make<T extends React.ComponentType<any>>(
|
||||
}
|
||||
|
||||
export const Renderers = {
|
||||
//#region Test
|
||||
TestMail: make(TestMail, 'Test Email from AFFiNE'),
|
||||
//#endregion
|
||||
|
||||
//#region User
|
||||
SignIn: make(SignIn, 'Sign in to AFFiNE'),
|
||||
SignUp: make(SignUp, 'Your AFFiNE account is waiting for you!'),
|
||||
|
||||
12
packages/backend/server/src/mails/test-mail.tsx
Normal file
12
packages/backend/server/src/mails/test-mail.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Content, P, Template, Title } from './components';
|
||||
|
||||
export default function TestMail() {
|
||||
return (
|
||||
<Template>
|
||||
<Title>Test Email from AFFiNE</Title>
|
||||
<Content>
|
||||
<P>This is a test email from your AFFiNE instance.</P>
|
||||
</Content>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
@@ -998,6 +998,7 @@ type Mutation {
|
||||
sendChangeEmail(callbackUrl: String!, email: String): Boolean!
|
||||
sendChangePasswordEmail(callbackUrl: String!, email: String @deprecated(reason: "fetched from signed in user")): Boolean!
|
||||
sendSetPasswordEmail(callbackUrl: String!, email: String @deprecated(reason: "fetched from signed in user")): Boolean!
|
||||
sendTestEmail(config: JSONObject!): Boolean!
|
||||
sendVerifyChangeEmail(callbackUrl: String!, email: String!, token: String!): Boolean!
|
||||
sendVerifyEmail(callbackUrl: String!): Boolean!
|
||||
setBlob(blob: Upload!, workspaceId: String!): String!
|
||||
|
||||
Reference in New Issue
Block a user