create-ej-app 14.0.0 → 16.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +234 -1
- package/package.json +3 -3
- package/template/base/package.json +38 -21
- package/template/base/src/app/(admin)/admin/page.tsx +3 -0
- package/template/base/src/app/api/[[...slugs]]/route.ts +25 -0
- package/template/base/src/emails/components/email-layout.tsx +63 -0
- package/template/base/src/emails/magic-link.tsx +49 -0
- package/template/base/src/emails/test.tsx +32 -0
- package/template/base/src/emails/welcome.tsx +34 -0
- package/template/base/src/lib/auth.ts +49 -15
- package/template/base/src/lib/eden.ts +7 -0
- package/template/base/src/lib/email-service.ts +70 -0
- package/template/base/src/lib/nodemailer.ts +43 -0
- package/template/base/src/scripts/test-mail.ts +29 -0
- package/template/base/terraform/cloudrun.tf +1 -1
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,5 +1,238 @@
|
|
|
1
1
|
# create-ej-app
|
|
2
2
|
|
|
3
|
+
A no-bullshit CLI to bootstrap a production-ready **fucking beast** of a app with Next.js, Better Auth, Drizzle ORM, Elysia, Workflow, shadcn/ui, React Email, and deployment wired up so you don’t screw your shit up in production.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
bunx create-ej-app@latest
|
|
3
7
|
```
|
|
4
|
-
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## ✨ Features (aka the good shit)
|
|
12
|
+
|
|
13
|
+
- **Next.js 16** – Powered by React 19, Server Components, App Router. Fast as hell.
|
|
14
|
+
- **Better Auth** – Auth that doesn’t suck:
|
|
15
|
+
- Email/Password
|
|
16
|
+
- Magic link (because passwords are annoying as fuck)
|
|
17
|
+
- OAuth with GitHub and Google
|
|
18
|
+
- Account linking
|
|
19
|
+
- Admin powers
|
|
20
|
+
|
|
21
|
+
- **Drizzle ORM** – Type-safe SQL over PostgreSQL without the usual ORM garbage.
|
|
22
|
+
- **Elysia** – Built for Bun. Minimal, fast, no nonsense.
|
|
23
|
+
- **Workflow** – Durable jobs so your background tasks don’t randomly die like weak shit.
|
|
24
|
+
- **React Email** – Build transactional emails using React components instead of ugly HTML nightmares.
|
|
25
|
+
- **shadcn/ui** – Clean, accessible components built on Radix UI.
|
|
26
|
+
- **TailwindCSS 4** – Utility-first styling with zero CSS drama.
|
|
27
|
+
- **TypeScript** – Strict types so you don’t ship dumb bugs.
|
|
28
|
+
- **TanStack Query** – Server state that doesn’t implode.
|
|
29
|
+
- **Lucide React** – Icons that don’t look like 2009 garbage.
|
|
30
|
+
- **Docker + Terraform** – Ship this shit properly.
|
|
31
|
+
- Terraform setup for Google Cloud Platform so you don’t YOLO infra.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 🚀 Quick Start (Let’s fucking go)
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
bunx create-ej-app@latest
|
|
5
39
|
```
|
|
40
|
+
|
|
41
|
+
You’ll be prompted for:
|
|
42
|
+
|
|
43
|
+
- Project name
|
|
44
|
+
- Description
|
|
45
|
+
- Template
|
|
46
|
+
- Git init (because not using git in 2026 is wild)
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
? Enter the project name: badass-app
|
|
52
|
+
? Enter a description: My savage Next.js monster
|
|
53
|
+
? Select a template: base
|
|
54
|
+
? Initialize a git repository? yes
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Boom. Done.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 📦 What You Actually Get
|
|
62
|
+
|
|
63
|
+
### Tech Stack (The real weapons)
|
|
64
|
+
|
|
65
|
+
- **Framework**: Next.js
|
|
66
|
+
- **Runtime**: Bun (fast as shit)
|
|
67
|
+
- **Backend**: Elysia APIs
|
|
68
|
+
- **Database**: PostgreSQL + Drizzle
|
|
69
|
+
- **Auth**: Better Auth
|
|
70
|
+
- **Emails**: React Email (transactional + auth templates ready)
|
|
71
|
+
- **Background Jobs**: Workflow
|
|
72
|
+
- **UI**: shadcn/ui
|
|
73
|
+
- **Styling**: TailwindCSS
|
|
74
|
+
- **State**: TanStack Query
|
|
75
|
+
- **Forms**: react-hook-form + Zod
|
|
76
|
+
- **Theme**: Dark mode out of the box
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 🛠 Setup & Development
|
|
81
|
+
|
|
82
|
+
### Prerequisites
|
|
83
|
+
|
|
84
|
+
- Node 24+ or Bun
|
|
85
|
+
- PostgreSQL running
|
|
86
|
+
- OAuth creds if you want GitHub/Google login
|
|
87
|
+
- Email provider (Resend, SES, Postmark, etc.)
|
|
88
|
+
|
|
89
|
+
### Install
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
cd my-app
|
|
93
|
+
bun install
|
|
94
|
+
# or npm / pnpm / yarn if that’s your thing
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
### Environment Variables
|
|
100
|
+
|
|
101
|
+
Copy `.env.example` → `.env` and fill your secrets like a responsible adult.
|
|
102
|
+
|
|
103
|
+
Don’t commit this crap. Seriously.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## 💻 Development
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
bun run dev
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Open `http://localhost:3000` and admire your badass setup.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 🐳 Docker (Ship it like a pro)
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
docker build -t my-app .
|
|
121
|
+
docker run -p 3000:3000 my-app
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
No “works on my machine” bullshit anymore.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## ☁️ Infrastructure (The Cloud Shit)
|
|
129
|
+
|
|
130
|
+
Terraform config included for:
|
|
131
|
+
|
|
132
|
+
- Cloud Run
|
|
133
|
+
- Cloud SQL
|
|
134
|
+
- Artifact Registry
|
|
135
|
+
- Cloud Build
|
|
136
|
+
- Secret Manager
|
|
137
|
+
- Load Balancer
|
|
138
|
+
- VPC
|
|
139
|
+
|
|
140
|
+
Deploy it:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
cd terraform
|
|
144
|
+
terraform init
|
|
145
|
+
terraform apply
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
And now you’re running production infra like a grown-ass engineer.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## 🔐 Authentication
|
|
153
|
+
|
|
154
|
+
Better Auth gives you:
|
|
155
|
+
|
|
156
|
+
- Email/Password login
|
|
157
|
+
- Magic links (no password drama)
|
|
158
|
+
- OAuth (GitHub, Google)
|
|
159
|
+
- Admin panel
|
|
160
|
+
- User impersonation
|
|
161
|
+
|
|
162
|
+
Secure, extensible, no duct-taped auth spaghetti.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## 🎨 UI Components
|
|
167
|
+
|
|
168
|
+
Preinstalled:
|
|
169
|
+
|
|
170
|
+
- Button
|
|
171
|
+
- Card
|
|
172
|
+
- Form
|
|
173
|
+
- Input
|
|
174
|
+
- Toast notifications
|
|
175
|
+
|
|
176
|
+
Need more?
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
bunx shadcn@latest add [component-name]
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Ship beautiful shit without reinventing buttons for the 900th time.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## 🎯 Route Groups
|
|
187
|
+
|
|
188
|
+
Organized like a sane human:
|
|
189
|
+
|
|
190
|
+
- `(admin)` – Admin dashboard
|
|
191
|
+
- `(app)` – Main app (auth required)
|
|
192
|
+
- `(auth)` – Login/signup
|
|
193
|
+
- `(marketing)` – Public pages
|
|
194
|
+
|
|
195
|
+
No messy folder hell.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## 🔧 Customization
|
|
200
|
+
|
|
201
|
+
Edit:
|
|
202
|
+
|
|
203
|
+
- `siteConfig.ts` – change name/description
|
|
204
|
+
- `schema.ts` – change DB schema
|
|
205
|
+
- `auth.ts` – tweak providers
|
|
206
|
+
|
|
207
|
+
Then push schema and move on with your life.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## 🤝 Contributing
|
|
212
|
+
|
|
213
|
+
PRs welcome. Don’t submit half-broken shit.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## 📄 License
|
|
218
|
+
|
|
219
|
+
MIT. Do whatever the hell you want, just don’t blame us if you screw it up.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## 🔗 Links
|
|
224
|
+
|
|
225
|
+
- Repository: [https://github.com/ejekanshjain/create-ej-app](https://github.com/ejekanshjain/create-ej-app)
|
|
226
|
+
- Issues: [https://github.com/ejekanshjain/create-ej-app/issues](https://github.com/ejekanshjain/create-ej-app/issues)
|
|
227
|
+
- Docs:
|
|
228
|
+
- [Next.js](https://nextjs.org/docs)
|
|
229
|
+
- [Better Auth](https://better-auth.com)
|
|
230
|
+
- [Drizzle ORM](https://orm.drizzle.team)
|
|
231
|
+
- [Elysia](https://elysiajs.com)
|
|
232
|
+
- [Workflow](https://useworkflow.dev)
|
|
233
|
+
- [shadcn/ui](https://ui.shadcn.com)
|
|
234
|
+
- [React Email](https://react.email)
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
Now go build some badass production-ready shit instead of another half-baked side project. 🚀
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-ej-app",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "16.0.0",
|
|
4
4
|
"description": "Bootstrap a next.js app with my preferred setup",
|
|
5
5
|
"main": "cli.js",
|
|
6
6
|
"scripts": {
|
|
@@ -30,12 +30,12 @@
|
|
|
30
30
|
},
|
|
31
31
|
"homepage": "https://github.com/ejekanshjain/create-ej-app#readme",
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"commander": "^14.0.
|
|
33
|
+
"commander": "^14.0.3",
|
|
34
34
|
"copy-dir": "^1.3.0",
|
|
35
35
|
"enquirer": "^2.4.1"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@types/node": "^25.
|
|
38
|
+
"@types/node": "^25.2.3",
|
|
39
39
|
"typescript": "^5.9.3"
|
|
40
40
|
}
|
|
41
41
|
}
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
"private": true,
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "next dev",
|
|
8
|
+
"email:dev": "email dev --dir src/emails",
|
|
9
|
+
"email:test": "bun ./src/scripts/test-mail.ts",
|
|
10
|
+
"workflow:inspect": "workflow inspect runs --web",
|
|
8
11
|
"build": "bun --bun next build",
|
|
9
12
|
"build:docker": "docker build -t my-app .",
|
|
10
13
|
"analyze": "ANALYZE=true bun --bun next build --webpack",
|
|
@@ -17,50 +20,64 @@
|
|
|
17
20
|
"gen:auth": "bunx @better-auth/cli generate"
|
|
18
21
|
},
|
|
19
22
|
"dependencies": {
|
|
23
|
+
"@ejekanshjain/simple-email-validator": "1.0.3",
|
|
24
|
+
"@elysiajs/eden": "1.4.8",
|
|
20
25
|
"@hookform/resolvers": "5.2.2",
|
|
21
|
-
"@next/third-parties": "16.1.
|
|
22
|
-
"@paralleldrive/cuid2": "3.0
|
|
26
|
+
"@next/third-parties": "16.1.6",
|
|
27
|
+
"@paralleldrive/cuid2": "3.3.0",
|
|
23
28
|
"@radix-ui/react-label": "2.1.8",
|
|
24
29
|
"@radix-ui/react-separator": "1.1.8",
|
|
25
30
|
"@radix-ui/react-slot": "1.2.4",
|
|
31
|
+
"@react-email/components": "1.0.7",
|
|
32
|
+
"@react-email/render": "2.0.4",
|
|
26
33
|
"@t3-oss/env-nextjs": "0.13.10",
|
|
27
|
-
"@tanstack/react-query": "5.90.
|
|
28
|
-
"better-auth": "1.4.
|
|
34
|
+
"@tanstack/react-query": "5.90.21",
|
|
35
|
+
"better-auth": "1.4.18",
|
|
29
36
|
"class-variance-authority": "0.7.1",
|
|
30
37
|
"clsx": "2.1.1",
|
|
31
38
|
"drizzle-orm": "0.45.1",
|
|
32
|
-
"
|
|
33
|
-
"lucide-react": "0.
|
|
34
|
-
"next": "16.1.
|
|
39
|
+
"elysia": "1.4.25",
|
|
40
|
+
"lucide-react": "0.563.0",
|
|
41
|
+
"next": "16.1.6",
|
|
35
42
|
"next-safe-action": "8.0.11",
|
|
36
43
|
"next-themes": "0.4.6",
|
|
37
44
|
"nextjs-toploader": "3.9.17",
|
|
38
|
-
"nodemailer": "
|
|
45
|
+
"nodemailer": "8.0.1",
|
|
39
46
|
"postgres": "3.4.8",
|
|
40
|
-
"react": "19.2.
|
|
41
|
-
"react-dom": "19.2.
|
|
42
|
-
"react-hook-form": "7.
|
|
47
|
+
"react": "19.2.4",
|
|
48
|
+
"react-dom": "19.2.4",
|
|
49
|
+
"react-hook-form": "7.71.1",
|
|
43
50
|
"sonner": "2.0.7",
|
|
44
51
|
"tailwind-merge": "3.4.0",
|
|
45
52
|
"tw-animate-css": "1.4.0",
|
|
46
|
-
"workflow": "4.0
|
|
47
|
-
"zod": "4.3.
|
|
53
|
+
"workflow": "4.1.0-beta.56",
|
|
54
|
+
"zod": "4.3.6"
|
|
48
55
|
},
|
|
49
56
|
"devDependencies": {
|
|
50
|
-
"@next/bundle-analyzer": "16.1.
|
|
57
|
+
"@next/bundle-analyzer": "16.1.6",
|
|
58
|
+
"@react-email/preview-server": "5.2.8",
|
|
51
59
|
"@tailwindcss/postcss": "4.1.18",
|
|
52
|
-
"@types/bun": "1.3.
|
|
53
|
-
"@types/nodemailer": "7.0.
|
|
54
|
-
"@types/react": "19.2.
|
|
60
|
+
"@types/bun": "1.3.9",
|
|
61
|
+
"@types/nodemailer": "7.0.9",
|
|
62
|
+
"@types/react": "19.2.14",
|
|
55
63
|
"@types/react-dom": "19.2.3",
|
|
56
64
|
"babel-plugin-react-compiler": "1.0.0",
|
|
57
|
-
"drizzle-kit": "0.31.
|
|
65
|
+
"drizzle-kit": "0.31.9",
|
|
58
66
|
"eslint": "9.39.2",
|
|
59
|
-
"eslint-config-next": "16.1.
|
|
60
|
-
"prettier": "3.
|
|
67
|
+
"eslint-config-next": "16.1.6",
|
|
68
|
+
"prettier": "3.8.1",
|
|
61
69
|
"prettier-plugin-organize-imports": "4.3.0",
|
|
62
70
|
"prettier-plugin-tailwindcss": "0.7.2",
|
|
71
|
+
"react-email": "5.2.8",
|
|
63
72
|
"tailwindcss": "4.1.18",
|
|
64
73
|
"typescript": "5.9.3"
|
|
65
|
-
}
|
|
74
|
+
},
|
|
75
|
+
"trustedDependencies": [
|
|
76
|
+
"@nestjs/core",
|
|
77
|
+
"@swc/core",
|
|
78
|
+
"cbor-extract",
|
|
79
|
+
"esbuild",
|
|
80
|
+
"sharp",
|
|
81
|
+
"unrs-resolver"
|
|
82
|
+
]
|
|
66
83
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Elysia } from 'elysia'
|
|
2
|
+
import z from 'zod'
|
|
3
|
+
|
|
4
|
+
export const elysiaApi = new Elysia({ prefix: '/api' })
|
|
5
|
+
.get('/', 'Hello from Elysia')
|
|
6
|
+
.post(
|
|
7
|
+
'/',
|
|
8
|
+
({ body }) => {
|
|
9
|
+
return `Hello, ${body.name}!`
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
body: z.object({
|
|
13
|
+
name: z.string().trim().min(1).describe('Name of the user')
|
|
14
|
+
}),
|
|
15
|
+
detail: {
|
|
16
|
+
description: 'Greet a user by name'
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
export const GET = elysiaApi.fetch
|
|
22
|
+
export const POST = elysiaApi.fetch
|
|
23
|
+
export const PUT = elysiaApi.fetch
|
|
24
|
+
export const PATCH = elysiaApi.fetch
|
|
25
|
+
export const DELETE = elysiaApi.fetch
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Body,
|
|
3
|
+
Container,
|
|
4
|
+
Head,
|
|
5
|
+
Hr,
|
|
6
|
+
Html,
|
|
7
|
+
pixelBasedPreset,
|
|
8
|
+
Preview,
|
|
9
|
+
Section,
|
|
10
|
+
Tailwind,
|
|
11
|
+
Text
|
|
12
|
+
} from '@react-email/components'
|
|
13
|
+
|
|
14
|
+
interface EmailLayoutProps {
|
|
15
|
+
previewText: string
|
|
16
|
+
companyName: string
|
|
17
|
+
children: React.ReactNode
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default function EmailLayout({
|
|
21
|
+
previewText,
|
|
22
|
+
companyName,
|
|
23
|
+
children
|
|
24
|
+
}: EmailLayoutProps) {
|
|
25
|
+
return (
|
|
26
|
+
<Html>
|
|
27
|
+
<Head />
|
|
28
|
+
<Preview>{previewText}</Preview>
|
|
29
|
+
<Tailwind
|
|
30
|
+
config={{
|
|
31
|
+
presets: [pixelBasedPreset]
|
|
32
|
+
}}
|
|
33
|
+
>
|
|
34
|
+
<Body className="bg-gray-100 font-sans">
|
|
35
|
+
<Container className="mx-auto my-8 max-w-xl rounded-lg bg-white shadow-sm">
|
|
36
|
+
{/* Header */}
|
|
37
|
+
<Section className="rounded-t-lg bg-black px-8 py-6">
|
|
38
|
+
<Text className="m-0 text-center text-2xl font-bold text-white">
|
|
39
|
+
{companyName}
|
|
40
|
+
</Text>
|
|
41
|
+
</Section>
|
|
42
|
+
|
|
43
|
+
{/* Content */}
|
|
44
|
+
<Section className="px-8 py-6">{children}</Section>
|
|
45
|
+
|
|
46
|
+
{/* Footer */}
|
|
47
|
+
<Section className="rounded-b-lg bg-gray-50 px-8 py-6">
|
|
48
|
+
<Hr className="my-4 border-gray-200" />
|
|
49
|
+
<Text className="m-0 text-center text-sm text-gray-500">
|
|
50
|
+
© {new Date().getUTCFullYear()} {companyName}. All rights
|
|
51
|
+
reserved.
|
|
52
|
+
</Text>
|
|
53
|
+
<Text className="m-0 mt-2 text-center text-xs text-gray-400">
|
|
54
|
+
This is an automated email. Please do not reply directly to this
|
|
55
|
+
message.
|
|
56
|
+
</Text>
|
|
57
|
+
</Section>
|
|
58
|
+
</Container>
|
|
59
|
+
</Body>
|
|
60
|
+
</Tailwind>
|
|
61
|
+
</Html>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Button, Heading, Text } from '@react-email/components'
|
|
2
|
+
import EmailLayout from './components/email-layout'
|
|
3
|
+
|
|
4
|
+
export interface MagicLinkEmailProps {
|
|
5
|
+
magicLink: string
|
|
6
|
+
companyName: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function MagicLinkEmail({
|
|
10
|
+
magicLink,
|
|
11
|
+
companyName
|
|
12
|
+
}: MagicLinkEmailProps) {
|
|
13
|
+
const previewText = `Your magic link to sign in to ${companyName}`
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<EmailLayout previewText={previewText} companyName={companyName}>
|
|
17
|
+
<Heading className="mb-4 text-2xl font-bold text-gray-900">
|
|
18
|
+
Sign in to {companyName}
|
|
19
|
+
</Heading>
|
|
20
|
+
|
|
21
|
+
<Text className="mb-4 text-base text-gray-700">
|
|
22
|
+
Click the button below to securely sign in to your account. This link
|
|
23
|
+
will expire in 5 minutes.
|
|
24
|
+
</Text>
|
|
25
|
+
|
|
26
|
+
<Button
|
|
27
|
+
href={magicLink}
|
|
28
|
+
className="mb-4 rounded-lg bg-black px-6 py-3 text-center text-base font-semibold text-white no-underline"
|
|
29
|
+
>
|
|
30
|
+
Sign In
|
|
31
|
+
</Button>
|
|
32
|
+
|
|
33
|
+
<Text className="mb-4 text-sm text-gray-600">
|
|
34
|
+
Or copy and paste this link into your browser:
|
|
35
|
+
</Text>
|
|
36
|
+
|
|
37
|
+
<Text className="mb-4 text-sm break-all text-blue-600">{magicLink}</Text>
|
|
38
|
+
|
|
39
|
+
<Text className="text-sm text-gray-500">
|
|
40
|
+
If you didn't request this email, you can safely ignore it.
|
|
41
|
+
</Text>
|
|
42
|
+
</EmailLayout>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
MagicLinkEmail.PreviewProps = {
|
|
47
|
+
magicLink: 'https://example.com/magic-link?token=abc123',
|
|
48
|
+
companyName: 'Example Site'
|
|
49
|
+
} satisfies MagicLinkEmailProps
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Button, Heading, Text } from '@react-email/components'
|
|
2
|
+
import EmailLayout from './components/email-layout'
|
|
3
|
+
|
|
4
|
+
interface TestEmailProps {
|
|
5
|
+
name: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function TestEmail({ name }: TestEmailProps) {
|
|
9
|
+
return (
|
|
10
|
+
<EmailLayout previewText="This is a test email" companyName="Test">
|
|
11
|
+
<Heading className="mb-4 text-2xl font-bold text-gray-900">
|
|
12
|
+
Test Email for {name}
|
|
13
|
+
</Heading>
|
|
14
|
+
<Text className="mb-4 text-base text-gray-700">
|
|
15
|
+
This is a test email template to verify email functionality.
|
|
16
|
+
</Text>
|
|
17
|
+
<Text className="mb-6 text-base text-gray-700">
|
|
18
|
+
If you're seeing this, the email system is working correctly.
|
|
19
|
+
</Text>
|
|
20
|
+
<Button
|
|
21
|
+
href="https://example.com"
|
|
22
|
+
className="rounded-md bg-black px-5 py-3 text-center text-sm font-medium text-white"
|
|
23
|
+
>
|
|
24
|
+
Test Button
|
|
25
|
+
</Button>
|
|
26
|
+
</EmailLayout>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
TestEmail.PreviewProps = {
|
|
31
|
+
name: 'John Doe'
|
|
32
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Heading, Text } from '@react-email/components'
|
|
2
|
+
import EmailLayout from './components/email-layout'
|
|
3
|
+
|
|
4
|
+
export interface WelcomeEmailProps {
|
|
5
|
+
userName: string
|
|
6
|
+
companyName: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function WelcomeEmail({
|
|
10
|
+
userName,
|
|
11
|
+
companyName
|
|
12
|
+
}: WelcomeEmailProps) {
|
|
13
|
+
const previewText = `Welcome to ${companyName}! Your account has been created successfully.`
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<EmailLayout previewText={previewText} companyName={companyName}>
|
|
17
|
+
<Heading className="mb-4 text-2xl font-bold text-gray-900">
|
|
18
|
+
Welcome to {companyName}! 🎉
|
|
19
|
+
</Heading>
|
|
20
|
+
|
|
21
|
+
<Text className="mb-4 text-base text-gray-700">Hi {userName},</Text>
|
|
22
|
+
|
|
23
|
+
<Text className="mb-4 text-base text-gray-700">
|
|
24
|
+
Thank you for creating an account with us. We're thrilled to have you as
|
|
25
|
+
part of our community!
|
|
26
|
+
</Text>
|
|
27
|
+
</EmailLayout>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
WelcomeEmail.PreviewProps = {
|
|
32
|
+
userName: 'John Doe',
|
|
33
|
+
companyName: 'Example Site'
|
|
34
|
+
} satisfies WelcomeEmailProps
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateEmail } from '@ejekanshjain/simple-email-validator'
|
|
1
2
|
import { createId } from '@paralleldrive/cuid2'
|
|
2
3
|
import { APIError, betterAuth } from 'better-auth'
|
|
3
4
|
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
|
|
@@ -7,9 +8,9 @@ import {
|
|
|
7
8
|
lastLoginMethod,
|
|
8
9
|
magicLink
|
|
9
10
|
} from 'better-auth/plugins'
|
|
10
|
-
import { isFakeEmail } from 'fakefilter'
|
|
11
11
|
import { headers } from 'next/headers'
|
|
12
12
|
import { cache } from 'react'
|
|
13
|
+
import { start } from 'workflow/api'
|
|
13
14
|
import { db } from '~/db'
|
|
14
15
|
import {
|
|
15
16
|
accountsTable,
|
|
@@ -18,6 +19,8 @@ import {
|
|
|
18
19
|
verificationsTable
|
|
19
20
|
} from '~/db/schema'
|
|
20
21
|
import { env } from '~/env'
|
|
22
|
+
import { sendMagicLinkEmail, sendWelcomeEmail } from './email-service'
|
|
23
|
+
import { siteConfig } from './siteConfig'
|
|
21
24
|
|
|
22
25
|
export const auth = betterAuth({
|
|
23
26
|
database: drizzleAdapter(db, {
|
|
@@ -51,14 +54,39 @@ export const auth = betterAuth({
|
|
|
51
54
|
clientSecret: env.BETTER_AUTH_GOOGLE_SECRET
|
|
52
55
|
}
|
|
53
56
|
},
|
|
57
|
+
databaseHooks: {
|
|
58
|
+
user: {
|
|
59
|
+
create: {
|
|
60
|
+
after: async user => {
|
|
61
|
+
try {
|
|
62
|
+
await start(sendWelcomeEmail, [
|
|
63
|
+
user.email,
|
|
64
|
+
{
|
|
65
|
+
userName: user.name,
|
|
66
|
+
companyName: siteConfig.name
|
|
67
|
+
}
|
|
68
|
+
])
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error('Failed to send welcome email:', error)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
},
|
|
54
76
|
plugins: [
|
|
55
77
|
magicLink({
|
|
56
|
-
sendMagicLink: async ({ email,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
78
|
+
sendMagicLink: async ({ email, url }) => {
|
|
79
|
+
try {
|
|
80
|
+
await start(sendMagicLinkEmail, [
|
|
81
|
+
email,
|
|
82
|
+
{
|
|
83
|
+
magicLink: url,
|
|
84
|
+
companyName: siteConfig.name
|
|
85
|
+
}
|
|
86
|
+
])
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error('Failed to send magic link email:', error)
|
|
89
|
+
}
|
|
62
90
|
}
|
|
63
91
|
}),
|
|
64
92
|
admin(),
|
|
@@ -66,14 +94,20 @@ export const auth = betterAuth({
|
|
|
66
94
|
],
|
|
67
95
|
hooks: {
|
|
68
96
|
before: createAuthMiddleware(async ctx => {
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
97
|
+
if (ctx.path === '/sign-in/magic-link' && ctx.body?.email) {
|
|
98
|
+
try {
|
|
99
|
+
const validation = await validateEmail({
|
|
100
|
+
email: ctx.body.email
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
if (validation.isValid === false) {
|
|
104
|
+
throw new APIError('BAD_REQUEST', {
|
|
105
|
+
message: 'Please use a valid email address.'
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.error('Email validation failed', err)
|
|
110
|
+
}
|
|
77
111
|
}
|
|
78
112
|
})
|
|
79
113
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { render, toPlainText } from '@react-email/render'
|
|
2
|
+
import MagicLinkEmail, { MagicLinkEmailProps } from '~/emails/magic-link'
|
|
3
|
+
import WelcomeEmail, { WelcomeEmailProps } from '~/emails/welcome'
|
|
4
|
+
import { sendEmail } from './nodemailer'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Mapping of template types to their corresponding props
|
|
8
|
+
*/
|
|
9
|
+
type EmailTemplateMap = {
|
|
10
|
+
welcome: WelcomeEmailProps
|
|
11
|
+
magicLink: MagicLinkEmailProps
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type EmailTemplateType = keyof EmailTemplateMap
|
|
15
|
+
|
|
16
|
+
// Map of email template components with proper type safety
|
|
17
|
+
const componentMap: Record<
|
|
18
|
+
EmailTemplateType,
|
|
19
|
+
(props: EmailTemplateMap[EmailTemplateType]) => React.ReactElement
|
|
20
|
+
> = {
|
|
21
|
+
welcome: props => WelcomeEmail(props as WelcomeEmailProps),
|
|
22
|
+
magicLink: props => MagicLinkEmail(props as MagicLinkEmailProps)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function renderEmailTemplate<T extends EmailTemplateType>(
|
|
26
|
+
templateType: T,
|
|
27
|
+
props: EmailTemplateMap[T]
|
|
28
|
+
): Promise<{ html: string; text: string }>
|
|
29
|
+
|
|
30
|
+
async function renderEmailTemplate(
|
|
31
|
+
templateType: EmailTemplateType,
|
|
32
|
+
props: EmailTemplateMap[EmailTemplateType]
|
|
33
|
+
) {
|
|
34
|
+
'use step'
|
|
35
|
+
|
|
36
|
+
const emailComponent = componentMap[templateType](props)
|
|
37
|
+
|
|
38
|
+
const html = await render(emailComponent)
|
|
39
|
+
const text = toPlainText(html)
|
|
40
|
+
return { html, text }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function sendWelcomeEmail(to: string, props: WelcomeEmailProps) {
|
|
44
|
+
'use workflow'
|
|
45
|
+
|
|
46
|
+
const { html, text } = await renderEmailTemplate('welcome', props)
|
|
47
|
+
|
|
48
|
+
return await sendEmail({
|
|
49
|
+
to,
|
|
50
|
+
subject: `Welcome to ${props.companyName}! 🎉`,
|
|
51
|
+
html,
|
|
52
|
+
text
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function sendMagicLinkEmail(
|
|
57
|
+
to: string,
|
|
58
|
+
props: MagicLinkEmailProps
|
|
59
|
+
) {
|
|
60
|
+
'use workflow'
|
|
61
|
+
|
|
62
|
+
const { html, text } = await renderEmailTemplate('magicLink', props)
|
|
63
|
+
|
|
64
|
+
return await sendEmail({
|
|
65
|
+
to,
|
|
66
|
+
subject: `Sign in to ${props.companyName}`,
|
|
67
|
+
html,
|
|
68
|
+
text
|
|
69
|
+
})
|
|
70
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import nodemailer, { SentMessageInfo } from 'nodemailer'
|
|
2
|
+
import { env } from '~/env'
|
|
3
|
+
|
|
4
|
+
const transporter = nodemailer.createTransport({
|
|
5
|
+
host: env.EMAIL_SERVER_HOST,
|
|
6
|
+
port: parseInt(env.EMAIL_SERVER_PORT),
|
|
7
|
+
secure: parseInt(env.EMAIL_SERVER_PORT) === 465, // Use secure for port 465, false for other ports
|
|
8
|
+
auth: {
|
|
9
|
+
user: env.EMAIL_SERVER_USER,
|
|
10
|
+
pass: env.EMAIL_SERVER_PASSWORD
|
|
11
|
+
}
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
interface SendEmailParams {
|
|
15
|
+
to: string
|
|
16
|
+
subject: string
|
|
17
|
+
text?: string
|
|
18
|
+
html?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const sendEmail = async ({
|
|
22
|
+
to,
|
|
23
|
+
subject,
|
|
24
|
+
text,
|
|
25
|
+
html
|
|
26
|
+
}: SendEmailParams): Promise<SentMessageInfo> => {
|
|
27
|
+
'use step'
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const info = await transporter.sendMail({
|
|
31
|
+
from: env.EMAIL_FROM,
|
|
32
|
+
to,
|
|
33
|
+
subject,
|
|
34
|
+
text,
|
|
35
|
+
html
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
return info
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error('Error sending email:', error)
|
|
41
|
+
throw new Error('Failed to send email')
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { render, toPlainText } from '@react-email/render'
|
|
2
|
+
import TestEmail from '~/emails/test'
|
|
3
|
+
import { sendEmail } from '~/lib/nodemailer'
|
|
4
|
+
|
|
5
|
+
async function main() {
|
|
6
|
+
const html = await render(
|
|
7
|
+
TestEmail({
|
|
8
|
+
name: 'Ekansh Jain'
|
|
9
|
+
}),
|
|
10
|
+
{
|
|
11
|
+
pretty: true
|
|
12
|
+
}
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
const text = toPlainText(html)
|
|
16
|
+
|
|
17
|
+
const result = await sendEmail({
|
|
18
|
+
to: 'john@example.com',
|
|
19
|
+
subject: 'Test Email',
|
|
20
|
+
text,
|
|
21
|
+
html
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
console.info('Test email sent successfully', result)
|
|
25
|
+
|
|
26
|
+
process.exit()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
main()
|