@yoms/create-monorepo 2.0.0 → 4.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/README.md +132 -4
- package/dist/index.js +113 -4
- package/package.json +1 -1
- package/templates/backend-hono/base/{AGENT.md → CLAUDE.md} +42 -3
- package/templates/backend-hono/features/better-auth/env-additions.txt +4 -0
- package/templates/backend-hono/features/better-auth/package-additions.json +5 -0
- package/templates/backend-hono/features/better-auth/prisma/schema.prisma +69 -0
- package/templates/backend-hono/features/better-auth/src/config/env-auth.ts +8 -0
- package/templates/backend-hono/features/better-auth/src/index.ts +53 -0
- package/templates/backend-hono/features/better-auth/src/lib/auth.ts +21 -0
- package/templates/backend-hono/features/better-auth/src/middleware/auth.middleware.ts +38 -0
- package/templates/frontend-nextjs/base/CLAUDE.md +183 -0
- package/templates/frontend-nextjs/base/app/layout.tsx +4 -1
- package/templates/frontend-nextjs/base/components/examples/users-list-example.tsx +127 -0
- package/templates/frontend-nextjs/base/lib/auth-client.ts +18 -0
- package/templates/frontend-nextjs/base/package.json +4 -1
- package/templates/frontend-nextjs/base/providers/query-provider.tsx +28 -0
- package/templates/frontend-nextjs/base/services/README.md +184 -0
- package/templates/frontend-nextjs/base/{lib/api-client.ts → services/api/client.ts} +1 -0
- package/templates/frontend-nextjs/base/services/api/endpoints.ts +26 -0
- package/templates/frontend-nextjs/base/services/api/index.ts +6 -0
- package/templates/frontend-nextjs/base/services/auth/auth.hook.ts +61 -0
- package/templates/frontend-nextjs/base/services/auth/auth.types.ts +38 -0
- package/templates/frontend-nextjs/base/services/auth/index.ts +12 -0
- package/templates/frontend-nextjs/base/services/users/index.ts +8 -0
- package/templates/frontend-nextjs/base/services/users/users.hook.ts +119 -0
- package/templates/frontend-nextjs/base/services/users/users.queries.ts +14 -0
- package/templates/frontend-nextjs/base/services/users/users.service.ts +65 -0
- package/templates/frontend-nextjs/base/services/users/users.types.ts +37 -0
- package/templates/shared/base/CLAUDE.md +95 -0
- package/templates/backend-hono/features/jwt-auth/env-additions.txt +0 -5
- package/templates/backend-hono/features/jwt-auth/package-additions.json +0 -10
- package/templates/backend-hono/features/jwt-auth/src/config/env-additions.ts +0 -16
- package/templates/backend-hono/features/jwt-auth/src/lib/jwt.ts +0 -75
- package/templates/backend-hono/features/jwt-auth/src/middleware/auth.middleware.ts +0 -50
- package/templates/backend-hono/features/jwt-auth/src/routes/auth.route.ts +0 -157
package/README.md
CHANGED
|
@@ -33,8 +33,10 @@ npm create @yoms/monorepo my-project
|
|
|
33
33
|
|
|
34
34
|
## Quick Start
|
|
35
35
|
|
|
36
|
+
### Interactive Mode (Default)
|
|
37
|
+
|
|
36
38
|
```bash
|
|
37
|
-
# Create a new project
|
|
39
|
+
# Create a new project with interactive prompts
|
|
38
40
|
npx @yoms/create-monorepo my-project
|
|
39
41
|
|
|
40
42
|
# Navigate to project
|
|
@@ -51,6 +53,49 @@ pnpm dev
|
|
|
51
53
|
# - Frontend: http://localhost:3000
|
|
52
54
|
```
|
|
53
55
|
|
|
56
|
+
### Non-Interactive Mode with CLI Flags
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# Create a project with all features using CLI flags
|
|
60
|
+
npx @yoms/create-monorepo my-project \
|
|
61
|
+
--backend hono \
|
|
62
|
+
--database postgres \
|
|
63
|
+
--redis \
|
|
64
|
+
--smtp \
|
|
65
|
+
--swagger \
|
|
66
|
+
--auth \
|
|
67
|
+
--frontend \
|
|
68
|
+
--shadcn \
|
|
69
|
+
--docker \
|
|
70
|
+
--pm pnpm
|
|
71
|
+
|
|
72
|
+
# Quick start with defaults (skips all prompts)
|
|
73
|
+
npx @yoms/create-monorepo my-project --yes
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## CLI Options
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npx @yoms/create-monorepo [dir] [options]
|
|
80
|
+
|
|
81
|
+
Options:
|
|
82
|
+
--backend <framework> Backend framework (hono)
|
|
83
|
+
--database <db> Database (postgres, mongodb, none)
|
|
84
|
+
--redis Include Redis cache
|
|
85
|
+
--smtp Include SMTP email
|
|
86
|
+
--swagger Include Swagger docs
|
|
87
|
+
--auth Include JWT authentication
|
|
88
|
+
--frontend Include Next.js frontend
|
|
89
|
+
--shadcn Include shadcn/ui components
|
|
90
|
+
--pm <manager> Package manager (pnpm, npm, yarn, bun)
|
|
91
|
+
--docker Include Docker Compose setup
|
|
92
|
+
--skip-install Skip dependency installation
|
|
93
|
+
--skip-git Skip git initialization
|
|
94
|
+
-y, --yes Skip all prompts and use defaults
|
|
95
|
+
-h, --help Display help
|
|
96
|
+
-v, --version Display version
|
|
97
|
+
```
|
|
98
|
+
|
|
54
99
|
## What You Get
|
|
55
100
|
|
|
56
101
|
### Backend Options
|
|
@@ -60,8 +105,12 @@ pnpm dev
|
|
|
60
105
|
- **Caching**: Redis with ready-to-use cache service
|
|
61
106
|
- **Email**: SMTP with Nodemailer
|
|
62
107
|
- **Docs**: Swagger/OpenAPI with Scalar UI
|
|
108
|
+
- **Authentication**: JWT with refresh tokens (optional)
|
|
63
109
|
- **Logging**: Winston with structured logging
|
|
64
110
|
- **Validation**: Zod schemas
|
|
111
|
+
- **Error Handling**: Custom error classes and response helpers
|
|
112
|
+
- **Rate Limiting**: Memory-based rate limiter with presets
|
|
113
|
+
- **Testing**: Vitest with example tests
|
|
65
114
|
- **Type Safety**: Full TypeScript with strict mode
|
|
66
115
|
|
|
67
116
|
### Frontend
|
|
@@ -85,12 +134,14 @@ my-project/
|
|
|
85
134
|
│ ├── api/ # Backend API
|
|
86
135
|
│ │ ├── src/
|
|
87
136
|
│ │ │ ├── routes/ # API routes
|
|
88
|
-
│ │ │ ├── middleware/ #
|
|
137
|
+
│ │ │ ├── middleware/ # Hono middleware
|
|
89
138
|
│ │ │ ├── services/ # Business logic
|
|
90
139
|
│ │ │ ├── config/ # Configuration (env, logger, db)
|
|
140
|
+
│ │ │ ├── lib/ # Utilities (jwt, errors, response)
|
|
91
141
|
│ │ │ └── types/ # TypeScript types
|
|
92
|
-
│ │ ├── prisma/ # Database schema
|
|
93
|
-
│ │
|
|
142
|
+
│ │ ├── prisma/ # Database schema (if database selected)
|
|
143
|
+
│ │ ├── Dockerfile # Production container
|
|
144
|
+
│ │ └── vitest.config.ts # Test configuration
|
|
94
145
|
│ │
|
|
95
146
|
│ ├── web/ # Next.js frontend
|
|
96
147
|
│ │ ├── app/ # App router pages
|
|
@@ -102,6 +153,7 @@ my-project/
|
|
|
102
153
|
│ ├── schemas/ # Zod schemas
|
|
103
154
|
│ └── types.ts # Common types
|
|
104
155
|
│
|
|
156
|
+
├── docker-compose.yml # Docker services (if --docker)
|
|
105
157
|
├── pnpm-workspace.yaml # Workspace configuration
|
|
106
158
|
└── tsconfig.base.json # Shared TypeScript config
|
|
107
159
|
```
|
|
@@ -157,6 +209,35 @@ SMTP_HOST=smtp.gmail.com
|
|
|
157
209
|
SMTP_PORT=587
|
|
158
210
|
SMTP_USER=...
|
|
159
211
|
SMTP_PASS=...
|
|
212
|
+
|
|
213
|
+
# JWT (if --auth selected)
|
|
214
|
+
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
|
|
215
|
+
JWT_EXPIRES_IN=15m
|
|
216
|
+
JWT_REFRESH_SECRET=your-super-secret-refresh-key-change-this-in-production
|
|
217
|
+
JWT_REFRESH_EXPIRES_IN=7d
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Docker Compose
|
|
221
|
+
|
|
222
|
+
When using the `--docker` flag, a `docker-compose.yml` file is generated with the following services based on your configuration:
|
|
223
|
+
|
|
224
|
+
- **PostgreSQL**: If `--database postgres` is selected
|
|
225
|
+
- **MongoDB**: If `--database mongodb` is selected
|
|
226
|
+
- **Redis**: If `--redis` is selected
|
|
227
|
+
- **MailHog**: If `--smtp` is selected (SMTP testing server with web UI)
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
# Start all services
|
|
231
|
+
docker-compose up -d
|
|
232
|
+
|
|
233
|
+
# View logs
|
|
234
|
+
docker-compose logs -f
|
|
235
|
+
|
|
236
|
+
# Stop services
|
|
237
|
+
docker-compose down
|
|
238
|
+
|
|
239
|
+
# View MailHog web UI (if SMTP selected)
|
|
240
|
+
open http://localhost:8025
|
|
160
241
|
```
|
|
161
242
|
|
|
162
243
|
## Available Scripts
|
|
@@ -165,11 +246,16 @@ SMTP_PASS=...
|
|
|
165
246
|
# Development
|
|
166
247
|
pnpm dev # Start all packages in dev mode
|
|
167
248
|
pnpm --filter api dev # Start only backend
|
|
249
|
+
pnpm --filter web dev # Start only frontend
|
|
168
250
|
|
|
169
251
|
# Building
|
|
170
252
|
pnpm build # Build all packages
|
|
171
253
|
pnpm typecheck # Type-check all packages
|
|
172
254
|
|
|
255
|
+
# Testing
|
|
256
|
+
pnpm --filter api test # Run backend tests
|
|
257
|
+
pnpm --filter api test:watch # Run tests in watch mode
|
|
258
|
+
|
|
173
259
|
# Database (if Prisma is selected)
|
|
174
260
|
pnpm --filter api prisma:generate # Generate Prisma client
|
|
175
261
|
pnpm --filter api prisma:migrate # Run migrations
|
|
@@ -180,6 +266,48 @@ pnpm lint # Lint all packages
|
|
|
180
266
|
pnpm format # Format with Prettier
|
|
181
267
|
```
|
|
182
268
|
|
|
269
|
+
## Authentication (JWT)
|
|
270
|
+
|
|
271
|
+
When using the `--auth` flag, JWT authentication is set up with the following features:
|
|
272
|
+
|
|
273
|
+
- **Access tokens**: Short-lived (15 minutes default)
|
|
274
|
+
- **Refresh tokens**: Long-lived (7 days default)
|
|
275
|
+
- **Password hashing**: bcryptjs with salt rounds
|
|
276
|
+
- **Protected routes**: Auth middleware for route protection
|
|
277
|
+
|
|
278
|
+
### Example Usage
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
// Register a new user
|
|
282
|
+
POST /auth/register
|
|
283
|
+
{
|
|
284
|
+
"email": "user@example.com",
|
|
285
|
+
"password": "securepassword",
|
|
286
|
+
"name": "John Doe"
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Login
|
|
290
|
+
POST /auth/login
|
|
291
|
+
{
|
|
292
|
+
"email": "user@example.com",
|
|
293
|
+
"password": "securepassword"
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Refresh access token
|
|
297
|
+
POST /auth/refresh
|
|
298
|
+
{
|
|
299
|
+
"refreshToken": "..."
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Protected route example
|
|
303
|
+
import { authMiddleware } from './middleware/auth.middleware';
|
|
304
|
+
|
|
305
|
+
app.get('/protected', authMiddleware, (c) => {
|
|
306
|
+
const { userId, email } = c.get('jwtPayload');
|
|
307
|
+
return c.json({ userId, email });
|
|
308
|
+
});
|
|
309
|
+
```
|
|
310
|
+
|
|
183
311
|
## Requirements
|
|
184
312
|
|
|
185
313
|
- Node.js >= 18
|
package/dist/index.js
CHANGED
|
@@ -199,11 +199,11 @@ async function promptBackendConfig() {
|
|
|
199
199
|
includeSwagger = swagger;
|
|
200
200
|
}
|
|
201
201
|
let includeAuth = false;
|
|
202
|
-
if (backendType === "web") {
|
|
202
|
+
if (backendType === "web" && database !== "none") {
|
|
203
203
|
const { auth } = await enquirer2.prompt({
|
|
204
204
|
type: "confirm",
|
|
205
205
|
name: "auth",
|
|
206
|
-
message: "Include
|
|
206
|
+
message: "Include authentication? (better-auth \u2014 email/password + sessions)",
|
|
207
207
|
initial: false
|
|
208
208
|
});
|
|
209
209
|
includeAuth = auth;
|
|
@@ -265,6 +265,7 @@ var BaseGenerator = class {
|
|
|
265
265
|
__PROJECT_NAME__: this.options.projectName,
|
|
266
266
|
__PACKAGE_SCOPE__: `@${this.options.projectName}`,
|
|
267
267
|
__DATABASE_PROVIDER__: "none",
|
|
268
|
+
__PRISMA_PROVIDER__: "postgresql",
|
|
268
269
|
__HAS_REDIS__: "false",
|
|
269
270
|
__HAS_SMTP__: "false",
|
|
270
271
|
__API_PORT__: "3001",
|
|
@@ -357,6 +358,113 @@ tmp/
|
|
|
357
358
|
`;
|
|
358
359
|
await writeFile(path2.join(this.options.projectDir, ".gitignore"), gitignoreContent);
|
|
359
360
|
const tokens = this.getTokens();
|
|
361
|
+
const pm = this.options.packageManager;
|
|
362
|
+
const hasBackend = config.includeBackend;
|
|
363
|
+
const hasFrontend = config.includeFrontend;
|
|
364
|
+
const hasShared = hasBackend && hasFrontend;
|
|
365
|
+
const db = config.backend?.database;
|
|
366
|
+
const hasAuth = config.backend?.includeAuth;
|
|
367
|
+
const hasRedis = config.backend?.includeRedis;
|
|
368
|
+
const claudeMdContent = `# CLAUDE.md \u2014 ${tokens.__PROJECT_NAME__}
|
|
369
|
+
|
|
370
|
+
This file provides guidance to Claude Code when working in this monorepo.
|
|
371
|
+
|
|
372
|
+
## Project Overview
|
|
373
|
+
|
|
374
|
+
**${tokens.__PROJECT_NAME__}** is a TypeScript monorepo generated with create-monorepo.
|
|
375
|
+
|
|
376
|
+
Packages:
|
|
377
|
+
${hasBackend ? `- \`packages/api/\` \u2014 Hono backend API (port ${tokens.__API_PORT__})
|
|
378
|
+
` : ""}${hasFrontend ? `- \`packages/web/\` \u2014 Next.js frontend (port ${tokens.__WEB_PORT__})
|
|
379
|
+
` : ""}${hasShared ? `- \`packages/shared/\` \u2014 Shared Zod schemas and TypeScript types
|
|
380
|
+
` : ""}
|
|
381
|
+
## Stack
|
|
382
|
+
|
|
383
|
+
${hasBackend ? `- **Backend**: Hono, TypeScript, Zod${db ? `, Prisma (${db})` : ""}${hasAuth ? ", better-auth" : ""}${hasRedis ? ", Redis" : ""}
|
|
384
|
+
` : ""}${hasFrontend ? `- **Frontend**: Next.js 15 App Router, TanStack Query, Tailwind CSS, shadcn/ui${hasAuth ? ", better-auth client" : ""}
|
|
385
|
+
` : ""}${hasShared ? `- **Shared**: Zod schemas, inferred TypeScript types
|
|
386
|
+
` : ""}- **Package Manager**: ${pm}
|
|
387
|
+
- **Monorepo**: pnpm workspaces
|
|
388
|
+
|
|
389
|
+
## Development Commands
|
|
390
|
+
|
|
391
|
+
\`\`\`bash
|
|
392
|
+
# Run everything
|
|
393
|
+
${pm} dev # Start all packages in parallel
|
|
394
|
+
|
|
395
|
+
# Individual packages
|
|
396
|
+
${pm} --filter api dev # Backend only
|
|
397
|
+
${pm} --filter web dev # Frontend only
|
|
398
|
+
|
|
399
|
+
# Build
|
|
400
|
+
${pm} build # Build all packages
|
|
401
|
+
${pm} --filter shared build # Build shared first if types changed
|
|
402
|
+
|
|
403
|
+
# Quality
|
|
404
|
+
${pm} typecheck # Type-check all packages
|
|
405
|
+
${pm} lint # Lint all packages
|
|
406
|
+
${pm} format # Format all packages
|
|
407
|
+
\`\`\`
|
|
408
|
+
|
|
409
|
+
## Package Dependency
|
|
410
|
+
|
|
411
|
+
\`\`\`
|
|
412
|
+
${hasShared ? `packages/shared \u2190 packages/api
|
|
413
|
+
\u2190 packages/web` : hasBackend ? "packages/api" : "packages/web"}
|
|
414
|
+
\`\`\`
|
|
415
|
+
|
|
416
|
+
${hasShared ? `> Always build \`packages/shared\` first after schema changes: \`${pm} --filter shared build\`
|
|
417
|
+
` : ""}${db ? `
|
|
418
|
+
## Database
|
|
419
|
+
|
|
420
|
+
- **Provider**: ${db === "postgres" ? "PostgreSQL" : "MongoDB"} via Prisma
|
|
421
|
+
- Schema: \`packages/api/prisma/schema.prisma\`
|
|
422
|
+
|
|
423
|
+
\`\`\`bash
|
|
424
|
+
${pm} --filter api prisma:generate # Regenerate Prisma client
|
|
425
|
+
${pm} --filter api prisma:migrate # Run migrations
|
|
426
|
+
${pm} --filter api prisma:studio # Open Prisma Studio
|
|
427
|
+
\`\`\`
|
|
428
|
+
` : ""}${hasAuth ? `
|
|
429
|
+
## Authentication
|
|
430
|
+
|
|
431
|
+
Powered by [better-auth](https://better-auth.com).
|
|
432
|
+
|
|
433
|
+
- Sessions stored in database, sent as HTTP-only cookies
|
|
434
|
+
- Auth routes handled at \`/api/auth/**\` on the backend
|
|
435
|
+
- Frontend uses \`lib/auth-client.ts\` \u2014 no manual token management
|
|
436
|
+
- Required env vars: \`BETTER_AUTH_SECRET\`, \`BETTER_AUTH_URL\`
|
|
437
|
+
|
|
438
|
+
See \`packages/api/CLAUDE.md\` for protecting routes.
|
|
439
|
+
See \`packages/web/CLAUDE.md\` for frontend usage.
|
|
440
|
+
` : ""}
|
|
441
|
+
## Environment Setup
|
|
442
|
+
|
|
443
|
+
\`\`\`bash
|
|
444
|
+
cp packages/api/.env.example packages/api/.env
|
|
445
|
+
${hasFrontend ? `cp packages/web/.env.example packages/web/.env
|
|
446
|
+
` : ""}\`\`\`
|
|
447
|
+
|
|
448
|
+
Edit the \`.env\` files before running \`${pm} dev\`.
|
|
449
|
+
|
|
450
|
+
## Key Files
|
|
451
|
+
|
|
452
|
+
${hasBackend ? `- \`packages/api/src/index.ts\` \u2014 Hono app entry point
|
|
453
|
+
- \`packages/api/src/lib/errors.ts\` \u2014 Custom error classes
|
|
454
|
+
- \`packages/api/src/lib/response.ts\` \u2014 Response helpers
|
|
455
|
+
` : ""}${hasFrontend ? `- \`packages/web/services/api/client.ts\` \u2014 HTTP client
|
|
456
|
+
- \`packages/web/services/api/endpoints.ts\` \u2014 API endpoint constants
|
|
457
|
+
- \`packages/web/lib/auth-client.ts\` \u2014 better-auth client
|
|
458
|
+
` : ""}${hasShared ? `- \`packages/shared/src/index.ts\` \u2014 All shared type exports
|
|
459
|
+
` : ""}
|
|
460
|
+
## Per-Package Guidance
|
|
461
|
+
|
|
462
|
+
Each package has its own \`CLAUDE.md\` with detailed patterns:
|
|
463
|
+
${hasBackend ? `- \`packages/api/CLAUDE.md\` \u2014 routes, services, middleware, error handling
|
|
464
|
+
` : ""}${hasFrontend ? `- \`packages/web/CLAUDE.md\` \u2014 services, hooks, auth, components
|
|
465
|
+
` : ""}${hasShared ? `- \`packages/shared/CLAUDE.md\` \u2014 adding schemas and types
|
|
466
|
+
` : ""}`;
|
|
467
|
+
await writeFile(path2.join(this.options.projectDir, "CLAUDE.md"), claudeMdContent);
|
|
360
468
|
const readmeContent = `# ${tokens.__PROJECT_NAME__}
|
|
361
469
|
|
|
362
470
|
Generated with create-monorepo.
|
|
@@ -530,6 +638,7 @@ var BackendGenerator = class extends BaseGenerator {
|
|
|
530
638
|
const tokens = this.getTokens({
|
|
531
639
|
__BACKEND_FRAMEWORK__: this.config.framework,
|
|
532
640
|
__DATABASE_PROVIDER__: this.config.database || "none",
|
|
641
|
+
__PRISMA_PROVIDER__: this.config.database === "postgres" ? "postgresql" : this.config.database === "mongodb" ? "mongodb" : "postgresql",
|
|
533
642
|
__HAS_REDIS__: String(this.config.includeRedis),
|
|
534
643
|
__HAS_SMTP__: String(this.config.includeSmtp)
|
|
535
644
|
});
|
|
@@ -561,8 +670,8 @@ var BackendGenerator = class extends BaseGenerator {
|
|
|
561
670
|
const swaggerFeaturePath = path4.join(featuresPath, "swagger");
|
|
562
671
|
await mergeFeature(backendDir, swaggerFeaturePath, tokens);
|
|
563
672
|
}
|
|
564
|
-
if (this.config.includeAuth) {
|
|
565
|
-
const authFeaturePath = path4.join(featuresPath, "
|
|
673
|
+
if (this.config.includeAuth && this.config.database) {
|
|
674
|
+
const authFeaturePath = path4.join(featuresPath, "better-auth");
|
|
566
675
|
await mergeFeature(backendDir, authFeaturePath, tokens);
|
|
567
676
|
}
|
|
568
677
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# CLAUDE.md — Backend API (`__PACKAGE_SCOPE__/api`)
|
|
2
2
|
|
|
3
|
-
This file provides guidance to
|
|
3
|
+
This file provides guidance to Claude Code when working in this package.
|
|
4
4
|
|
|
5
5
|
## Project Overview
|
|
6
6
|
|
|
@@ -315,10 +315,49 @@ logger.error('Database error', { error, query });
|
|
|
315
315
|
- Use `prisma.$transaction()` for multiple related operations
|
|
316
316
|
- Profile with `pnpm test:coverage` to find slow tests
|
|
317
317
|
|
|
318
|
+
## Authentication (better-auth)
|
|
319
|
+
|
|
320
|
+
If auth is enabled, the project uses [better-auth](https://better-auth.com) with cookie-based sessions.
|
|
321
|
+
|
|
322
|
+
### How it works
|
|
323
|
+
|
|
324
|
+
- All auth routes are mounted at `/api/auth/**` — handled automatically by better-auth
|
|
325
|
+
- Sessions are stored in the database (Prisma adapter)
|
|
326
|
+
- Cookies are HTTP-only — no manual token management
|
|
327
|
+
|
|
328
|
+
### Protecting routes
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
import { requireAuth, optionalAuth } from '../middleware/auth.middleware.js';
|
|
332
|
+
|
|
333
|
+
// Require a valid session
|
|
334
|
+
app.get('/profile', requireAuth, (c) => {
|
|
335
|
+
const session = c.get('session'); // { user: User, session: Session }
|
|
336
|
+
return success(c, session.user);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// Attach session if present, but don't block
|
|
340
|
+
app.get('/feed', optionalAuth, (c) => {
|
|
341
|
+
const session = c.get('session'); // null if not logged in
|
|
342
|
+
...
|
|
343
|
+
});
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Prisma schema
|
|
347
|
+
|
|
348
|
+
The schema includes `user`, `session`, `account`, and `verification` tables managed by better-auth. Do not modify them manually — run `npx better-auth generate` if you change `src/lib/auth.ts`.
|
|
349
|
+
|
|
350
|
+
### Auth environment variables
|
|
351
|
+
|
|
352
|
+
```
|
|
353
|
+
BETTER_AUTH_SECRET=... # min 32 chars
|
|
354
|
+
BETTER_AUTH_URL=... # your API URL, e.g. http://localhost:3001
|
|
355
|
+
```
|
|
356
|
+
|
|
318
357
|
## Security Checklist
|
|
319
358
|
|
|
320
359
|
- [ ] All inputs are validated with Zod
|
|
321
|
-
- [ ] Rate limiting is applied to
|
|
360
|
+
- [ ] Rate limiting is applied to sensitive endpoints
|
|
322
361
|
- [ ] Sensitive data is not logged
|
|
323
362
|
- [ ] Database queries use parameterized queries (Prisma does this)
|
|
324
363
|
- [ ] CORS is configured correctly
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
generator client {
|
|
2
|
+
provider = "prisma-client-js"
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
datasource db {
|
|
6
|
+
provider = "__PRISMA_PROVIDER__"
|
|
7
|
+
url = env("DATABASE_URL")
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Better Auth models — managed by better-auth, do not modify manually.
|
|
11
|
+
// Run `npx better-auth generate` to regenerate if you change auth config.
|
|
12
|
+
|
|
13
|
+
model user {
|
|
14
|
+
id String @id
|
|
15
|
+
name String
|
|
16
|
+
email String @unique
|
|
17
|
+
emailVerified Boolean
|
|
18
|
+
image String?
|
|
19
|
+
createdAt DateTime
|
|
20
|
+
updatedAt DateTime
|
|
21
|
+
sessions session[]
|
|
22
|
+
accounts account[]
|
|
23
|
+
|
|
24
|
+
@@map("user")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
model session {
|
|
28
|
+
id String @id
|
|
29
|
+
expiresAt DateTime
|
|
30
|
+
token String @unique
|
|
31
|
+
createdAt DateTime
|
|
32
|
+
updatedAt DateTime
|
|
33
|
+
ipAddress String?
|
|
34
|
+
userAgent String?
|
|
35
|
+
userId String
|
|
36
|
+
user user @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
37
|
+
|
|
38
|
+
@@map("session")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
model account {
|
|
42
|
+
id String @id
|
|
43
|
+
accountId String
|
|
44
|
+
providerId String
|
|
45
|
+
userId String
|
|
46
|
+
user user @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
47
|
+
accessToken String?
|
|
48
|
+
refreshToken String?
|
|
49
|
+
idToken String?
|
|
50
|
+
accessTokenExpiresAt DateTime?
|
|
51
|
+
refreshTokenExpiresAt DateTime?
|
|
52
|
+
scope String?
|
|
53
|
+
password String?
|
|
54
|
+
createdAt DateTime
|
|
55
|
+
updatedAt DateTime
|
|
56
|
+
|
|
57
|
+
@@map("account")
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
model verification {
|
|
61
|
+
id String @id
|
|
62
|
+
identifier String
|
|
63
|
+
value String
|
|
64
|
+
expiresAt DateTime
|
|
65
|
+
createdAt DateTime?
|
|
66
|
+
updatedAt DateTime?
|
|
67
|
+
|
|
68
|
+
@@map("verification")
|
|
69
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
const authEnvSchema = z.object({
|
|
4
|
+
BETTER_AUTH_SECRET: z.string().min(32, 'BETTER_AUTH_SECRET must be at least 32 characters'),
|
|
5
|
+
BETTER_AUTH_URL: z.string().url('BETTER_AUTH_URL must be a valid URL'),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export const authEnv = authEnvSchema.parse(process.env);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { env } from './config/env.js';
|
|
3
|
+
import { logger } from './config/logger.js';
|
|
4
|
+
import { corsMiddleware } from './middleware/cors.middleware.js';
|
|
5
|
+
import { loggerMiddleware } from './middleware/logger.middleware.js';
|
|
6
|
+
import { errorHandler } from './middleware/error.middleware.js';
|
|
7
|
+
import { health } from './routes/health.route.js';
|
|
8
|
+
import { auth } from './lib/auth.js';
|
|
9
|
+
|
|
10
|
+
const app = new Hono();
|
|
11
|
+
|
|
12
|
+
// Global middleware
|
|
13
|
+
app.use('*', corsMiddleware);
|
|
14
|
+
app.use('*', loggerMiddleware);
|
|
15
|
+
|
|
16
|
+
// Better Auth — handles all /api/auth/* routes (sign-in, sign-up, sign-out, session, etc.)
|
|
17
|
+
app.on(['POST', 'GET'], '/api/auth/**', (c) => auth.handler(c.req.raw));
|
|
18
|
+
|
|
19
|
+
// Routes
|
|
20
|
+
app.route('/health', health);
|
|
21
|
+
|
|
22
|
+
// Root endpoint
|
|
23
|
+
app.get('/', (c) => {
|
|
24
|
+
return c.json({
|
|
25
|
+
success: true,
|
|
26
|
+
message: 'Welcome to __PROJECT_NAME__ API',
|
|
27
|
+
version: '0.1.0',
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Error handling
|
|
32
|
+
app.onError(errorHandler);
|
|
33
|
+
|
|
34
|
+
// 404 handler
|
|
35
|
+
app.notFound((c) => {
|
|
36
|
+
return c.json({
|
|
37
|
+
success: false,
|
|
38
|
+
error: 'Not found',
|
|
39
|
+
message: `Route ${c.req.method} ${c.req.url} not found`,
|
|
40
|
+
}, 404);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Start server
|
|
44
|
+
const port = env.PORT;
|
|
45
|
+
|
|
46
|
+
logger.info(`Starting server in ${env.NODE_ENV} mode...`);
|
|
47
|
+
|
|
48
|
+
export default {
|
|
49
|
+
port,
|
|
50
|
+
fetch: app.fetch,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
console.log(`Server running on http://localhost:${port}`);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { betterAuth } from 'better-auth';
|
|
2
|
+
import { prismaAdapter } from 'better-auth/adapters/prisma';
|
|
3
|
+
import { PrismaClient } from '@prisma/client';
|
|
4
|
+
import { authEnv } from '../config/env-auth.js';
|
|
5
|
+
|
|
6
|
+
const prisma = new PrismaClient();
|
|
7
|
+
|
|
8
|
+
export const auth = betterAuth({
|
|
9
|
+
baseURL: authEnv.BETTER_AUTH_URL,
|
|
10
|
+
secret: authEnv.BETTER_AUTH_SECRET,
|
|
11
|
+
database: prismaAdapter(prisma, {
|
|
12
|
+
provider: '__PRISMA_PROVIDER__',
|
|
13
|
+
}),
|
|
14
|
+
emailAndPassword: {
|
|
15
|
+
enabled: true,
|
|
16
|
+
},
|
|
17
|
+
trustedOrigins: [process.env.WEB_URL || 'http://localhost:__WEB_PORT__'],
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export type Session = typeof auth.$Infer.Session;
|
|
21
|
+
export type User = typeof auth.$Infer.Session.user;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Context, Next } from 'hono';
|
|
2
|
+
import { auth } from '../lib/auth.js';
|
|
3
|
+
import { UnauthorizedError } from '../lib/errors.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Middleware to require an authenticated session.
|
|
7
|
+
* Attaches the session to c.get('session') on success.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* app.get('/protected', requireAuth, (c) => {
|
|
11
|
+
* const session = c.get('session');
|
|
12
|
+
* return c.json({ user: session.user });
|
|
13
|
+
* });
|
|
14
|
+
*/
|
|
15
|
+
export async function requireAuth(c: Context, next: Next): Promise<void> {
|
|
16
|
+
const session = await auth.api.getSession({ headers: c.req.raw.headers });
|
|
17
|
+
|
|
18
|
+
if (!session) {
|
|
19
|
+
throw new UnauthorizedError('Authentication required');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
c.set('session', session);
|
|
23
|
+
await next();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Optional auth middleware — attaches session if present, continues either way.
|
|
28
|
+
* Use for routes that behave differently for authenticated vs. anonymous users.
|
|
29
|
+
*/
|
|
30
|
+
export async function optionalAuth(c: Context, next: Next): Promise<void> {
|
|
31
|
+
const session = await auth.api.getSession({ headers: c.req.raw.headers });
|
|
32
|
+
|
|
33
|
+
if (session) {
|
|
34
|
+
c.set('session', session);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
await next();
|
|
38
|
+
}
|