Skip to content

Commit

Permalink
add email package for email templating (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomasfrancisco authored Oct 3, 2024
1 parent a055338 commit a762eaa
Show file tree
Hide file tree
Showing 21 changed files with 1,770 additions and 93 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ certificates
# vite timestamp - remove after it's fixed: https://github.com/vitejs/vite/issues/13267
**/vite.config.ts.timestamp-*

# app specific
apps/engine/public/static/email

# package specific
packages/components/src/components
Expand Down
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
auto-install-peers = true
enable-pre-post-scripts=true
public-hoist-pattern[]=*storybook*
2 changes: 1 addition & 1 deletion apps/engine/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pnpm dev
bun dev
```

Open [https://localhost:3000](https://localhost:3000) with your browser to see the result.
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.

Expand Down
11 changes: 9 additions & 2 deletions apps/engine/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
"type": "module",
"scripts": {
"build": "next build",
"dev": "next dev --experimental-https",
"dev": "next dev",
"format": "prettier --check . --ignore-path ../../.gitignore --ignore-path ../../.prettierignore",
"lint": "eslint",
"prebuild": "tsx ./scripts/copy-email-static-files.ts",
"predev": "tsx ./scripts/copy-email-static-files.ts",
"start": "next start",
"type-check": "tsc --noEmit"
},
Expand All @@ -20,6 +22,7 @@
"@ds-project/auth": "workspace:*",
"@ds-project/components": "workspace:*",
"@ds-project/database": "workspace:*",
"@ds-project/email": "workspace:*",
"@hookform/resolvers": "^3.9.0",
"@octokit/app": "^15.1.0",
"@octokit/auth-oauth-app": "^8.1.1",
Expand All @@ -45,7 +48,7 @@
"next": "catalog:",
"next-safe-action": "^7.8.1",
"postgres": "^3.4.4",
"posthog-js": "^1.160.0",
"posthog-js": "^1.166.0",
"rambda": "^9.2.1",
"react": "catalog:",
"react-diff-viewer": "^3.1.1",
Expand All @@ -55,6 +58,7 @@
"react-json-view-lite": "^1.4.0",
"server-only": "^0.0.1",
"sharp": "^0.33.5",
"standardwebhooks": "^1.0.0",
"style-dictionary": "catalog:",
"superjson": "^2.2.1",
"tailwind-merge": "^2.4.0",
Expand All @@ -68,16 +72,19 @@
"@ds-project/typescript": "workspace:*",
"@next/env": "^14.2.13",
"@octokit/types": "^13.5.0",
"@types/fs-extra": "^11.0.4",
"@types/node": "catalog:",
"@types/object-hash": "^3.0.6",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"drizzle-kit": "^0.24.2",
"eslint": "catalog:",
"eslint-config-next": "14.2.5",
"fs-extra": "^11.2.0",
"jiti": "^1.21.6",
"postcss": "catalog:",
"tailwindcss": "catalog:",
"tsx": "^4.19.1",
"typescript": "catalog:"
}
}
33 changes: 33 additions & 0 deletions apps/engine/scripts/copy-email-static-files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import * as fs from 'fs-extra';
import * as path from 'path';
import * as url from 'node:url';

const __filename = url.fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const rootDir = '../../..';
const emailDir = '/packages/email/src/templates/static';
const engineDir = '/apps/engine/public/static/email';

const staticSrcDir = path.join(__dirname, `${rootDir}${emailDir}`);
const staticDestDir = path.join(__dirname, `${rootDir}${engineDir}`);

async function copyStaticFiles() {
try {
await fs.ensureDir(staticDestDir);
await fs.copy(staticSrcDir, staticDestDir);
console.log(`
✨ Email static files copied successfully.
📄 Summary:
${emailDir} ➡️ ${engineDir}
`);
} catch (err) {
console.error('💥 Error copying email static files:', err);
}
}

async function main() {
await copyStaticFiles();
}

main();
62 changes: 62 additions & 0 deletions apps/engine/src/app/api/email/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { serverEnv } from '@/env/server-env';
import { SignUpEmail } from '@ds-project/email/src/templates/sign-up';
import { Resend } from '@ds-project/email/src/resend';
import { render } from '@ds-project/email/src/render';
import { config } from '@/config';
import { Webhook } from 'standardwebhooks';

const resend = new Resend(serverEnv.RESEND_API_KEY);

export async function POST(request: NextRequest) {
const wh = new Webhook(serverEnv.SEND_EMAIL_HOOK_SECRET);
const payload = await request.text();
const headers = Object.fromEntries(request.headers);

try {
const {
user,
email_data: { token },
} = wh.verify(payload, headers) as {
user: {
email: string;
};
email_data: {
token: string;
};
};

const html = await render(
<SignUpEmail
otpCode={token}
staticPathUrl={`${config.pageUrl}/static/email`}
/>
);

const { error } = await resend.emails.send({
from: 'DS Pro <[email protected]>',
to: [user.email],
subject: 'DS Pro - Confirmation Code',
html,
});

if (error) {
throw error;
}
} catch (error) {
return NextResponse.json(
{ error },
{
status: 401,
}
);
}

return NextResponse.json(
{},
{
status: 200,
}
);
}
2 changes: 1 addition & 1 deletion apps/engine/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const pageUrl = (() => {
case 'preview':
return `https://${clientEnv.NEXT_PUBLIC_VERCEL_URL}`;
default:
return 'https://localhost:3000';
return 'http://localhost:3000';
}
})();

Expand Down
2 changes: 2 additions & 0 deletions apps/engine/src/env/server-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export const serverEnv = createEnv({
SENTRY_ORG: z.string().min(1).optional(),
SENTRY_PROJECT: z.string().min(1).optional(),
SENTRY_AUTH_TOKEN: z.string().min(1).optional(),
RESEND_API_KEY: z.string().min(1),
SEND_EMAIL_HOOK_SECRET: z.string().min(1),
// Feature Flags
ENABLE_RELEASES_FLAG: z.coerce.boolean(),
},
Expand Down
13 changes: 7 additions & 6 deletions apps/engine/src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ import { serverEnv } from './env/server-env';
export const config = {
/**
* Match all paths except for:
* 1. /api/ routes
* 2. /_next/ (Next.js internals)
* 3. /_proxy/m (Monitoring - Sentry proxy)
* 4. /_proxy/a (Analytics - PostHog proxy)
* 5. /*.* (static files like /favicon.ico, /sitemap.xml, /robots.txt, etc.)
* 1. /static/ (static files)
* 2. /api/ routes
* 3. /_next/ (Next.js internals)
* 4. /_proxy/m (Monitoring - Sentry proxy)
* 5. /_proxy/a (Analytics - PostHog proxy)
* 6. /*.* (static files like /favicon.ico, /sitemap.xml, /robots.txt, etc.)
*/
matcher: ['/((?!api/|_next/|_proxy/m|_proxy/a|[\\w-]+\\.\\w+).*)'],
matcher: ['/((?!static/|api/|_next/|_proxy/m|_proxy/a|[\\w-]+\\.\\w+).*)'],
};

export async function middleware(request: NextRequest) {
Expand Down
1 change: 1 addition & 0 deletions packages/database/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"lint": "eslint",
"migrate": "pnpx tsx --conditions=react-server src/migrate.ts",
"reset": "supabase db reset && pnpm migrate",
"restart": "supabase stop && supabase start",
"squash": "supabase migration squash --local",
"start": "supabase start",
"status": "supabase status",
Expand Down
15 changes: 10 additions & 5 deletions packages/database/supabase/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@ enabled = true
enabled = true
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
# in emails.
site_url = "https://localhost:3000"
site_url = "http://localhost:3000"
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
additional_redirect_urls = ["https://localhost:3000"]
additional_redirect_urls = ["http://localhost:3000"]
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
jwt_expiry = 3600
# If disabled, the refresh token will never expire.
Expand Down Expand Up @@ -121,9 +121,14 @@ max_frequency = "1s"
# sender_name = "Admin"

# Uncomment to customize email template
[auth.email.template.magic_link]
subject = "DS Pro - Confirmation code"
content_path = "./supabase/templates/invite.html"
# [auth.email.template.magic_link]
# subject = "DS Pro - Confirmation code"
# content_path = "./supabase/templates/invite.html"

[auth.hook.send_email]
enabled = true
uri = "http://host.docker.internal:3000/api/email"
secrets = "env(SUPABASE_AUTH_HOOK_SEND_EMAIL_SECRET)"

[auth.sms]
# Allow/disallow new user signups via SMS to your project.
Expand Down
11 changes: 11 additions & 0 deletions packages/email/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import baseConfig from '@ds-project/eslint/base';
import reactConfig from '@ds-project/eslint/react';

/** @type {import('typescript-eslint').Config} */
export default [
{
ignores: ['dist/**'],
},
...baseConfig,
...reactConfig,
];
29 changes: 29 additions & 0 deletions packages/email/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "@ds-project/email",
"private": true,
"keywords": [],
"license": "ISC",
"author": "",
"type": "module",
"scripts": {
"dev": "email dev --dir ./templates",
"format": "prettier --check . --ignore-path ../../.gitignore --ignore-path ../../.prettierignore",
"lint": "eslint",
"type-check": "tsc --noEmit --emitDeclarationOnly false"
},
"prettier": "@ds-project/prettier",
"dependencies": {
"@react-email/components": "0.0.24",
"@react-email/render": "^1.0.1",
"react": "catalog:",
"resend": "^4.0.0"
},
"devDependencies": {
"@ds-project/eslint": "workspace:*",
"@ds-project/prettier": "workspace:*",
"@ds-project/typescript": "workspace:*",
"@types/react": "catalog:",
"eslint": "catalog:",
"react-email": "3.0.1"
}
}
1 change: 1 addition & 0 deletions packages/email/src/render.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { render } from '@react-email/components';
1 change: 1 addition & 0 deletions packages/email/src/resend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from 'resend';
Loading

0 comments on commit a762eaa

Please sign in to comment.