From 2acd9c7d1044bad79ab96caec709b08a4de28395 Mon Sep 17 00:00:00 2001 From: giancarlo Date: Wed, 27 Mar 2024 14:10:53 +0800 Subject: [PATCH] Refactor mailer setup and validate web configuration in app This commit refactors how SMTP configuration for mailers is set up and introduces schema validation for incoming configurations. The mailer modules have been restructured, with schema definition files added, and redundant codes removed. Moreover, web application configuration now has minimum validation on name and title, and URL validation has been added. --- apps/web/config/app.config.ts | 26 ++++++-- apps/web/config/auth.config.ts | 8 ++- packages/features/auth/package.json | 4 +- .../components/sign-in-methods-container.tsx | 3 +- packages/mailers/src/impl/cloudflare/index.ts | 17 ++++++ packages/mailers/src/impl/nodemailer.ts | 59 ------------------- packages/mailers/src/impl/nodemailer/index.ts | 21 +++++++ packages/mailers/src/mailer.ts | 2 +- .../mailer.schema.ts} | 0 .../mailers/src/schema/smtp-config.schema.ts | 49 +++++++++++++++ packages/mailers/src/smtp-configuration.ts | 21 +++++++ pnpm-lock.yaml | 6 ++ 12 files changed, 146 insertions(+), 70 deletions(-) create mode 100644 packages/mailers/src/impl/cloudflare/index.ts delete mode 100644 packages/mailers/src/impl/nodemailer.ts create mode 100644 packages/mailers/src/impl/nodemailer/index.ts rename packages/mailers/src/{mailer-schema.ts => schema/mailer.schema.ts} (100%) create mode 100644 packages/mailers/src/schema/smtp-config.schema.ts create mode 100644 packages/mailers/src/smtp-configuration.ts diff --git a/apps/web/config/app.config.ts b/apps/web/config/app.config.ts index 5ca278a20..8430b0b56 100644 --- a/apps/web/config/app.config.ts +++ b/apps/web/config/app.config.ts @@ -8,11 +8,27 @@ enum Themes { } const AppConfigSchema = z.object({ - name: z.string(), - title: z.string(), - description: z.string(), - url: z.string(), - locale: z.string().default('en'), + name: z + .string({ + description: `This is the name of your SaaS. Ex. "Makerkit"`, + }) + .min(1), + title: z + .string({ + description: `This is the default title tag of your SaaS.`, + }) + .min(1), + description: z.string({ + description: `This is the default description of your SaaS.`, + }), + url: z.string().url({ + message: `Please provide a valid URL. Example: 'https://example.com'`, + }), + locale: z + .string({ + description: `This is the default locale of your SaaS.`, + }) + .default('en'), theme: z.nativeEnum(Themes), production: z.boolean(), themeColor: z.string(), diff --git a/apps/web/config/auth.config.ts b/apps/web/config/auth.config.ts index 2db320ad3..3f007d23d 100644 --- a/apps/web/config/auth.config.ts +++ b/apps/web/config/auth.config.ts @@ -6,8 +6,12 @@ const providers: z.ZodType = getProviders(); const AuthConfigSchema = z.object({ providers: z.object({ - password: z.boolean(), - magicLink: z.boolean(), + password: z.boolean({ + description: 'Enable password authentication.', + }), + magicLink: z.boolean({ + description: 'Enable magic link authentication.', + }), oAuth: providers.array(), }), }); diff --git a/packages/features/auth/package.json b/packages/features/auth/package.json index c8da612b3..1d470bb96 100644 --- a/packages/features/auth/package.json +++ b/packages/features/auth/package.json @@ -18,6 +18,7 @@ "devDependencies": { "@kit/eslint-config": "0.2.0", "@kit/prettier-config": "0.1.0", + "@kit/shared": "0.1.0", "@kit/supabase": "0.1.0", "@kit/tailwind-config": "0.1.0", "@kit/tsconfig": "0.1.0", @@ -25,7 +26,8 @@ "@radix-ui/react-icons": "^1.3.0", "@tanstack/react-query": "5.28.6", "react-i18next": "^14.1.0", - "sonner": "^1.4.41" + "sonner": "^1.4.41", + "zod": "^3.22.4" }, "prettier": "@kit/prettier-config", "eslintConfig": { diff --git a/packages/features/auth/src/components/sign-in-methods-container.tsx b/packages/features/auth/src/components/sign-in-methods-container.tsx index 9419beaad..4062aaa6f 100644 --- a/packages/features/auth/src/components/sign-in-methods-container.tsx +++ b/packages/features/auth/src/components/sign-in-methods-container.tsx @@ -4,8 +4,7 @@ import { useRouter, useSearchParams } from 'next/navigation'; import type { Provider } from '@supabase/supabase-js'; -import { isBrowser } from '@supabase/ssr'; - +import { isBrowser } from '@kit/shared/utils'; import { Divider } from '@kit/ui/divider'; import { If } from '@kit/ui/if'; diff --git a/packages/mailers/src/impl/cloudflare/index.ts b/packages/mailers/src/impl/cloudflare/index.ts new file mode 100644 index 000000000..3fcd9f466 --- /dev/null +++ b/packages/mailers/src/impl/cloudflare/index.ts @@ -0,0 +1,17 @@ +import 'server-only'; +import { z } from 'zod'; + +import { Mailer } from '../../mailer'; +import { MailerSchema } from '../../schema/mailer.schema'; + +type Config = z.infer; + +/** + * A class representing a mailer using Cloudflare's Workers. + * @implements {Mailer} + */ +export class CloudflareMailer implements Mailer { + async sendEmail(config: Config) { + throw new Error('Not implemented'); + } +} diff --git a/packages/mailers/src/impl/nodemailer.ts b/packages/mailers/src/impl/nodemailer.ts deleted file mode 100644 index 20f9d2a7c..000000000 --- a/packages/mailers/src/impl/nodemailer.ts +++ /dev/null @@ -1,59 +0,0 @@ -import 'server-only'; -import { z } from 'zod'; - -import { Mailer } from '../mailer'; -import { MailerSchema } from '../mailer-schema'; - -type Config = z.infer; - -/** - * A class representing a mailer using Nodemailer library. - * @implements {Mailer} - */ -export class Nodemailer implements Mailer { - async sendEmail(config: Config) { - const transporter = await getSMTPTransporter(); - - return transporter.sendMail(config); - } -} - -/** - * @description SMTP Transporter for production use. Add your favorite email - * API details (Mailgun, Sendgrid, etc.) to the appConfig. - */ -async function getSMTPTransporter() { - const { createTransport } = await import('nodemailer'); - - return createTransport(getSMTPConfiguration()); -} - -function getSMTPConfiguration() { - const user = process.env.EMAIL_USER; - const pass = process.env.EMAIL_PASSWORD; - const host = process.env.EMAIL_HOST; - const port = Number(process.env.EMAIL_PORT); - const secure = process.env.EMAIL_TLS !== 'false'; - - // validate that we have all the required appConfig - if (!user || !pass || !host || !port) { - throw new Error( - `Missing email configuration. Please add the following environment variables: - EMAIL_USER - EMAIL_PASSWORD - EMAIL_HOST - EMAIL_PORT - `, - ); - } - - return { - host, - port, - secure, - auth: { - user, - pass, - }, - }; -} diff --git a/packages/mailers/src/impl/nodemailer/index.ts b/packages/mailers/src/impl/nodemailer/index.ts new file mode 100644 index 000000000..c3623001b --- /dev/null +++ b/packages/mailers/src/impl/nodemailer/index.ts @@ -0,0 +1,21 @@ +import 'server-only'; +import { z } from 'zod'; + +import { Mailer } from '../../mailer'; +import { MailerSchema } from '../../schema/mailer.schema'; +import { getSMTPConfiguration } from '../../smtp-configuration'; + +type Config = z.infer; + +/** + * A class representing a mailer using Nodemailer library. + * @implements {Mailer} + */ +export class Nodemailer implements Mailer { + async sendEmail(config: Config) { + const { createTransport } = await import('nodemailer'); + const transporter = createTransport(getSMTPConfiguration()); + + return transporter.sendMail(config); + } +} diff --git a/packages/mailers/src/mailer.ts b/packages/mailers/src/mailer.ts index c07289704..ab4578956 100644 --- a/packages/mailers/src/mailer.ts +++ b/packages/mailers/src/mailer.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -import { MailerSchema } from './mailer-schema'; +import { MailerSchema } from './schema/mailer.schema'; export abstract class Mailer { abstract sendEmail(data: z.infer): Promise; diff --git a/packages/mailers/src/mailer-schema.ts b/packages/mailers/src/schema/mailer.schema.ts similarity index 100% rename from packages/mailers/src/mailer-schema.ts rename to packages/mailers/src/schema/mailer.schema.ts diff --git a/packages/mailers/src/schema/smtp-config.schema.ts b/packages/mailers/src/schema/smtp-config.schema.ts new file mode 100644 index 000000000..55a97aeb9 --- /dev/null +++ b/packages/mailers/src/schema/smtp-config.schema.ts @@ -0,0 +1,49 @@ +import { z } from 'zod'; + +/* +const user = process.env.EMAIL_USER; + const pass = process.env.EMAIL_PASSWORD; + const host = process.env.EMAIL_HOST; + const port = Number(process.env.EMAIL_PORT); + const secure = process.env.EMAIL_TLS !== 'false'; + + // validate that we have all the required appConfig + if (!user || !pass || !host || !port) { + throw new Error( + `Missing email configuration. Please add the following environment variables: + EMAIL_USER + EMAIL_PASSWORD + EMAIL_HOST + EMAIL_PORT + `, + ); + } + + return { + host, + port, + secure, + auth: { + user, + pass, + }, + }; + */ + +export const SmtpConfigSchema = z.object({ + user: z.string({ + description: + 'This is the email account to send emails from. This is specific to the email provider.', + }), + pass: z.string({ + description: 'This is the password for the email account', + }), + host: z.string({ + description: 'This is the SMTP host for the email provider', + }), + port: z.number({ + description: + 'This is the port for the email provider. Normally 587 or 465.', + }), + secure: z.boolean(), +}); diff --git a/packages/mailers/src/smtp-configuration.ts b/packages/mailers/src/smtp-configuration.ts new file mode 100644 index 000000000..bc863604a --- /dev/null +++ b/packages/mailers/src/smtp-configuration.ts @@ -0,0 +1,21 @@ +import { SmtpConfigSchema } from './schema/smtp-config.schema'; + +export function getSMTPConfiguration() { + const data = SmtpConfigSchema.parse({ + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASSWORD, + host: process.env.EMAIL_HOST, + port: Number(process.env.EMAIL_PORT), + secure: process.env.EMAIL_TLS !== 'false', + }); + + return { + host: data.host, + port: data.port, + secure: data.secure, + auth: { + user: data.user, + pass: data.pass, + }, + }; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 586d3a075..47404b492 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -337,6 +337,9 @@ importers: '@kit/prettier-config': specifier: 0.1.0 version: link:../../../tooling/prettier + '@kit/shared': + specifier: 0.1.0 + version: link:../../shared '@kit/supabase': specifier: 0.1.0 version: link:../../supabase @@ -361,6 +364,9 @@ importers: sonner: specifier: ^1.4.41 version: 1.4.41(react-dom@18.2.0)(react@18.2.0) + zod: + specifier: ^3.22.4 + version: 3.22.4 packages/features/team-accounts: devDependencies: