From dad858014fbfea335893283b4b2a7332a3a8534a Mon Sep 17 00:00:00 2001 From: forehalo Date: Tue, 1 Apr 2025 15:00:10 +0000 Subject: [PATCH] feat(admin): adapt new config system (#11360) feat(server): add test mail api feat(admin): adapt new config system --- .../__tests__/__snapshots__/mails.spec.ts.md | 43 +++++ .../__snapshots__/mails.spec.ts.snap | Bin 4443 -> 4500 bytes .../backend/server/src/base/graphql/config.ts | 7 +- .../backend/server/src/base/graphql/index.ts | 6 + .../backend/server/src/core/mail/index.ts | 3 +- .../backend/server/src/core/mail/resolver.ts | 49 +++++ .../backend/server/src/core/mail/sender.ts | 32 ++-- .../server/src/mails/components/template.tsx | 2 +- packages/backend/server/src/mails/index.tsx | 7 +- .../backend/server/src/mails/test-mail.tsx | 12 ++ packages/backend/server/src/schema.gql | 1 + .../src/graphql/admin/send-test-email.gql | 10 ++ packages/common/graphql/src/graphql/index.ts | 10 ++ packages/common/graphql/src/schema.ts | 24 +++ packages/frontend/admin/src/app.tsx | 4 +- .../src/modules/{config => about}/about.tsx | 0 .../src/modules/{config => about}/index.tsx | 0 .../frontend/admin/src/modules/layout.tsx | 4 +- .../src/modules/nav/collapsible-item.tsx | 74 +------- .../frontend/admin/src/modules/nav/nav.tsx | 4 +- .../admin/src/modules/nav/settings-item.tsx | 62 ++++--- .../src/modules/settings/config-input-row.tsx | 170 ++++++++++++++++++ .../src/modules/settings/config-input.tsx | 132 -------------- .../admin/src/modules/settings/config.ts | 156 ++++++++++++++-- .../src/modules/settings/confirm-changes.tsx | 4 +- .../admin/src/modules/settings/index.tsx | 132 +++++++------- .../settings/operations/send-test-email.tsx | 32 ++++ .../modules/settings/runtime-setting-row.tsx | 30 ---- .../src/modules/settings/use-app-config.ts | 108 +++++++---- 29 files changed, 718 insertions(+), 400 deletions(-) create mode 100644 packages/backend/server/src/core/mail/resolver.ts create mode 100644 packages/backend/server/src/mails/test-mail.tsx create mode 100644 packages/common/graphql/src/graphql/admin/send-test-email.gql rename packages/frontend/admin/src/modules/{config => about}/about.tsx (100%) rename packages/frontend/admin/src/modules/{config => about}/index.tsx (100%) create mode 100644 packages/frontend/admin/src/modules/settings/config-input-row.tsx delete mode 100644 packages/frontend/admin/src/modules/settings/config-input.tsx create mode 100644 packages/frontend/admin/src/modules/settings/operations/send-test-email.tsx delete mode 100644 packages/frontend/admin/src/modules/settings/runtime-setting-row.tsx diff --git a/packages/backend/server/src/__tests__/__snapshots__/mails.spec.ts.md b/packages/backend/server/src/__tests__/__snapshots__/mails.spec.ts.md index 4796d6973b..58209f1540 100644 --- a/packages/backend/server/src/__tests__/__snapshots__/mails.spec.ts.md +++ b/packages/backend/server/src/__tests__/__snapshots__/mails.spec.ts.md @@ -6,6 +6,49 @@ Generated by [AVA](https://avajs.dev). ## should render emails +> Test Email from AFFiNE + + `␊ + ␊ + ␊ + ␊ + ␊ + ␊ + ␊ + Test Email from AFFiNE␊ +

␊ + ␊ + ␊ + ␊ + ␊ + ␊ + ␊ + ␊ + ␊ + ␊ + This is a test email from your AFFiNE instance.␊ +

␊ + ␊ + ␊ + ␊ + ␊ + ␊ + ` + > Sign in to AFFiNE `␊ diff --git a/packages/backend/server/src/__tests__/__snapshots__/mails.spec.ts.snap b/packages/backend/server/src/__tests__/__snapshots__/mails.spec.ts.snap index 97131d67b9b09cb13ea633e9371f238d5c4c8453..b95aed3e95839d2b9b8826b97c644a1e707f9343 100644 GIT binary patch literal 4500 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 diff --git a/packages/backend/server/src/base/graphql/config.ts b/packages/backend/server/src/base/graphql/config.ts index 1406322c0a..22989fe066 100644 --- a/packages/backend/server/src/base/graphql/config.ts +++ b/packages/backend/server/src/base/graphql/config.ts @@ -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', }, diff --git a/packages/backend/server/src/base/graphql/index.ts b/packages/backend/server/src/base/graphql/index.ts index 8338638a25..8f8811bc99 100644 --- a/packages/backend/server/src/base/graphql/index.ts +++ b/packages/backend/server/src/base/graphql/index.ts @@ -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 diff --git a/packages/backend/server/src/core/mail/index.ts b/packages/backend/server/src/core/mail/index.ts index 834c25c74d..ffa2429899 100644 --- a/packages/backend/server/src/core/mail/index.ts +++ b/packages/backend/server/src/core/mail/index.ts @@ -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 {} diff --git a/packages/backend/server/src/core/mail/resolver.ts b/packages/backend/server/src/core/mail/resolver.ts new file mode 100644 index 0000000000..78fe19147c --- /dev/null +++ b/packages/backend/server/src/core/mail/resolver.ts @@ -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; + } +} diff --git a/packages/backend/server/src/core/mail/sender.ts b/packages/backend/server/src/core/mail/sender.ts index fc3beba6b6..fbce9584a8 100644 --- a/packages/backend/server/src/core/mail/sender.ts +++ b/packages/backend/server/src/core/mail/sender.ts @@ -16,6 +16,22 @@ export type SendOptions = Omit & { 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); diff --git a/packages/backend/server/src/mails/components/template.tsx b/packages/backend/server/src/mails/components/template.tsx index fc4906f5f5..116cff3c74 100644 --- a/packages/backend/server/src/mails/components/template.tsx +++ b/packages/backend/server/src/mails/components/template.tsx @@ -195,7 +195,7 @@ export function Template(props: PropsWithChildren) { ); - if (env.testing) { + if (globalThis.env?.testing) { return content; } diff --git a/packages/backend/server/src/mails/index.tsx b/packages/backend/server/src/mails/index.tsx index 8f18ec62ed..d6e36240d0 100644 --- a/packages/backend/server/src/mails/index.tsx +++ b/packages/backend/server/src/mails/index.tsx @@ -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 extends React.FC ? P : never; +type Props = T extends React.ComponentType ? P : never; export type EmailRenderer = (props: Props) => Promise; function make>( @@ -65,6 +66,10 @@ function make>( } 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!'), diff --git a/packages/backend/server/src/mails/test-mail.tsx b/packages/backend/server/src/mails/test-mail.tsx new file mode 100644 index 0000000000..0407cafb42 --- /dev/null +++ b/packages/backend/server/src/mails/test-mail.tsx @@ -0,0 +1,12 @@ +import { Content, P, Template, Title } from './components'; + +export default function TestMail() { + return ( + + ); +} diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql index 81c31aa70f..07e1d10bbf 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -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! diff --git a/packages/common/graphql/src/graphql/admin/send-test-email.gql b/packages/common/graphql/src/graphql/admin/send-test-email.gql new file mode 100644 index 0000000000..7b0e94e9fa --- /dev/null +++ b/packages/common/graphql/src/graphql/admin/send-test-email.gql @@ -0,0 +1,10 @@ +mutation sendTestEmail($host: String!, $port: Int!, $sender: String!, $username: String!, $password: String!, $ignoreTLS: Boolean!) { + sendTestEmail(config: { + host: $host, + port: $port, + sender: $sender, + username: $username, + password: $password, + ignoreTLS: $ignoreTLS, + }) +} \ No newline at end of file diff --git a/packages/common/graphql/src/graphql/index.ts b/packages/common/graphql/src/graphql/index.ts index b72484147b..e1c314008e 100644 --- a/packages/common/graphql/src/graphql/index.ts +++ b/packages/common/graphql/src/graphql/index.ts @@ -220,6 +220,16 @@ export const listUsersQuery = { }`, }; +export const sendTestEmailMutation = { + id: 'sendTestEmailMutation' as const, + op: 'sendTestEmail', + query: `mutation sendTestEmail($host: String!, $port: Int!, $sender: String!, $username: String!, $password: String!, $ignoreTLS: Boolean!) { + sendTestEmail( + config: {host: $host, port: $port, sender: $sender, username: $username, password: $password, ignoreTLS: $ignoreTLS} + ) +}`, +}; + export const updateAccountFeaturesMutation = { id: 'updateAccountFeaturesMutation' as const, op: 'updateAccountFeatures', diff --git a/packages/common/graphql/src/schema.ts b/packages/common/graphql/src/schema.ts index d2f8d2be80..c153674fc6 100644 --- a/packages/common/graphql/src/schema.ts +++ b/packages/common/graphql/src/schema.ts @@ -1111,6 +1111,7 @@ export interface Mutation { sendChangeEmail: Scalars['Boolean']['output']; sendChangePasswordEmail: Scalars['Boolean']['output']; sendSetPasswordEmail: Scalars['Boolean']['output']; + sendTestEmail: Scalars['Boolean']['output']; sendVerifyChangeEmail: Scalars['Boolean']['output']; sendVerifyEmail: Scalars['Boolean']['output']; setBlob: Scalars['String']['output']; @@ -1404,6 +1405,10 @@ export interface MutationSendSetPasswordEmailArgs { email?: InputMaybe; } +export interface MutationSendTestEmailArgs { + config: Scalars['JSONObject']['input']; +} + export interface MutationSendVerifyChangeEmailArgs { callbackUrl: Scalars['String']['input']; email: Scalars['String']['input']; @@ -2509,6 +2514,20 @@ export type ListUsersQuery = { }>; }; +export type SendTestEmailMutationVariables = Exact<{ + host: Scalars['String']['input']; + port: Scalars['Int']['input']; + sender: Scalars['String']['input']; + username: Scalars['String']['input']; + password: Scalars['String']['input']; + ignoreTLS: Scalars['Boolean']['input']; +}>; + +export type SendTestEmailMutation = { + __typename?: 'Mutation'; + sendTestEmail: boolean; +}; + export type UpdateAccountFeaturesMutationVariables = Exact<{ userId: Scalars['String']['input']; features: Array | FeatureType; @@ -4587,6 +4606,11 @@ export type Mutations = variables: ImportUsersMutationVariables; response: ImportUsersMutation; } + | { + name: 'sendTestEmailMutation'; + variables: SendTestEmailMutationVariables; + response: SendTestEmailMutation; + } | { name: 'updateAccountFeaturesMutation'; variables: UpdateAccountFeaturesMutationVariables; diff --git a/packages/frontend/admin/src/app.tsx b/packages/frontend/admin/src/app.tsx index b7ee341395..f8ba35b90c 100644 --- a/packages/frontend/admin/src/app.tsx +++ b/packages/frontend/admin/src/app.tsx @@ -85,8 +85,8 @@ export const router = _createBrowserRouter( lazy: () => import('./modules/ai'), }, { - path: 'config', - lazy: () => import('./modules/config'), + path: 'about', + lazy: () => import('./modules/about'), }, { path: 'settings', diff --git a/packages/frontend/admin/src/modules/config/about.tsx b/packages/frontend/admin/src/modules/about/about.tsx similarity index 100% rename from packages/frontend/admin/src/modules/config/about.tsx rename to packages/frontend/admin/src/modules/about/about.tsx diff --git a/packages/frontend/admin/src/modules/config/index.tsx b/packages/frontend/admin/src/modules/about/index.tsx similarity index 100% rename from packages/frontend/admin/src/modules/config/index.tsx rename to packages/frontend/admin/src/modules/about/index.tsx diff --git a/packages/frontend/admin/src/modules/layout.tsx b/packages/frontend/admin/src/modules/layout.tsx index 7ea3a11b23..b53602c1ba 100644 --- a/packages/frontend/admin/src/modules/layout.tsx +++ b/packages/frontend/admin/src/modules/layout.tsx @@ -39,8 +39,8 @@ export function Layout({ children }: PropsWithChildren) { const leftPanelRef = useRef(null); const [activeTab, setActiveTab] = useState(''); - const [activeSubTab, setActiveSubTab] = useState('auth'); - const [currentModule, setCurrentModule] = useState('auth'); + const [activeSubTab, setActiveSubTab] = useState('server'); + const [currentModule, setCurrentModule] = useState('server'); const handleLeftExpand = useCallback(() => { if (leftPanelRef.current?.getSize() === 0) { diff --git a/packages/frontend/admin/src/modules/nav/collapsible-item.tsx b/packages/frontend/admin/src/modules/nav/collapsible-item.tsx index 753121f4bf..f82f1fe958 100644 --- a/packages/frontend/admin/src/modules/nav/collapsible-item.tsx +++ b/packages/frontend/admin/src/modules/nav/collapsible-item.tsx @@ -1,62 +1,25 @@ -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from '@affine/admin/components/ui/accordion'; import { useCallback } from 'react'; import { NavLink } from 'react-router-dom'; import { buttonVariants } from '../../components/ui/button'; import { cn } from '../../utils'; -export const CollapsibleItem = ({ - title, - changeModule, -}: { - title: string; - changeModule?: (module: string) => void; -}) => { - const handleClick = useCallback(() => { - changeModule?.(title); - }, [changeModule, title]); - return ( - - - { - return isActive - ? 'w-full bg-zinc-100 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50' - : ''; - }} - > - - {title} - - - - - ); -}; - export const NormalSubItem = ({ + module, title, changeModule, }: { + module: string; title: string; changeModule?: (module: string) => void; }) => { const handleClick = useCallback(() => { - changeModule?.(title); - }, [changeModule, title]); + changeModule?.(module); + }, [changeModule, module]); return (
{ return cn( @@ -72,30 +35,3 @@ export const NormalSubItem = ({
); }; - -export const OtherModules = ({ - moduleList, - changeModule, -}: { - moduleList: string[]; - changeModule?: (module: string) => void; -}) => { - return ( - - - - Other - - - {moduleList.map(module => ( - - ))} - - - - ); -}; diff --git a/packages/frontend/admin/src/modules/nav/nav.tsx b/packages/frontend/admin/src/modules/nav/nav.tsx index 5adfe7b8bf..a5f67edbd9 100644 --- a/packages/frontend/admin/src/modules/nav/nav.tsx +++ b/packages/frontend/admin/src/modules/nav/nav.tsx @@ -98,9 +98,9 @@ export function Nav({ isCollapsed = false }: NavProps) { /> } - label="Server" + label="About" isCollapsed={isCollapsed} /> diff --git a/packages/frontend/admin/src/modules/nav/settings-item.tsx b/packages/frontend/admin/src/modules/nav/settings-item.tsx index 3c57ac2588..df4e4459b3 100644 --- a/packages/frontend/admin/src/modules/nav/settings-item.tsx +++ b/packages/frontend/admin/src/modules/nav/settings-item.tsx @@ -12,15 +12,10 @@ import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'; import { cssVarV2 } from '@toeverything/theme/v2'; import { NavLink } from 'react-router-dom'; -import { ALL_CONFIGURABLE_MODULES } from '../settings/config'; -import { NormalSubItem, OtherModules } from './collapsible-item'; +import { KNOWN_CONFIG_GROUPS, UNKNOWN_CONFIG_GROUPS } from '../settings/config'; +import { NormalSubItem } from './collapsible-item'; import { useNav } from './context'; -const authModule = ALL_CONFIGURABLE_MODULES.find(module => module === 'auth'); -const otherModules = ALL_CONFIGURABLE_MODULES.filter( - module => module !== 'auth' -); - export const SettingsItem = ({ isCollapsed }: { isCollapsed: boolean }) => { const { setCurrentModule } = useNav(); @@ -59,10 +54,10 @@ export const SettingsItem = ({ isCollapsed }: { isCollapsed: boolean }) => { borderColor: cssVarV2('layer/insideBorder/blackBorder'), }} > - {authModule ? ( -
  • + {KNOWN_CONFIG_GROUPS.map(group => ( +
  • { ? cssVarV2('selfhost/button/sidebarButton/bg/select') : undefined, })} - onClick={() => setCurrentModule?.(authModule)} + onClick={() => setCurrentModule?.(group.module)} > - {authModule} + {group.name}
  • - ) : null} - {otherModules.map(module => ( -
  • + ))} + {UNKNOWN_CONFIG_GROUPS.map(group => ( +
  • { ? cssVarV2('selfhost/button/sidebarButton/bg/select') : undefined, })} - onClick={() => setCurrentModule?.(module)} + onClick={() => setCurrentModule?.(group.module)} > - {module} + {group.name}
  • ))} @@ -151,18 +146,31 @@ export const SettingsItem = ({ isCollapsed }: { isCollapsed: boolean }) => { className={cn('relative overflow-hidden w-full h-full')} > - {authModule && ( + {KNOWN_CONFIG_GROUPS.map(group => ( - )} - {otherModules.length > 0 && ( - - )} + ))} + + + + Experimental + + + {UNKNOWN_CONFIG_GROUPS.map(group => ( + + ))} + + + void; +} & ( + | { + type: 'String' | 'Number' | 'Boolean' | 'JSON'; + } + | { + type: 'Enum'; + options: string[]; + } +); + +const Inputs: Record< + ConfigInputProps['type'], + React.ComponentType<{ + defaultValue: any; + onChange: (value?: any) => void; + options?: string[]; + }> +> = { + Boolean: function SwitchInput({ defaultValue, onChange }) { + const handleSwitchChange = (checked: boolean) => { + onChange(checked); + }; + + return ( + + ); + }, + String: function StringInput({ defaultValue, onChange }) { + const handleInputChange = (e: React.ChangeEvent) => { + onChange(e.target.value); + }; + + return ( + + ); + }, + Number: function NumberInput({ defaultValue, onChange }) { + const handleInputChange = (e: React.ChangeEvent) => { + onChange(parseInt(e.target.value)); + }; + + return ( + + ); + }, + JSON: function ObjectInput({ defaultValue, onChange }) { + const handleInputChange = (e: React.ChangeEvent) => { + try { + const value = JSON.parse(e.target.value); + onChange(value); + } catch {} + }; + + return ( +