@erwininteractive/mvc 0.7.1 → 0.8.2
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 +182 -487
- package/dist/generators/initApp.js +19 -29
- package/package.json +3 -1
- package/templates/appScaffold/src/assets/tailwind.css +1 -3
- package/templates/ci/test.yml +1 -1
package/README.md
CHANGED
|
@@ -4,13 +4,21 @@ A lightweight, full-featured MVC framework for Node.js 20+ built with TypeScript
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **Express** - Fast,
|
|
8
|
-
- **EJS
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
7
|
+
- **Express** - Fast, unopinionated web framework for routing and middleware
|
|
8
|
+
- **EJS Templating** - Server-side templating with embedded JavaScript
|
|
9
|
+
- **Prisma ORM** - Type-safe database client (optional, PostgreSQL/MySQL/SQLite support)
|
|
10
|
+
- **Redis Sessions** - Scalable session management with connect-redis
|
|
11
11
|
- **JWT Authentication** - Secure token-based auth with bcrypt password hashing
|
|
12
|
+
- **Session-Based Auth** - Traditional session management with Express-Session
|
|
12
13
|
- **WebAuthn (Passkeys)** - Passwordless authentication with security keys (YubiKey, Touch ID, Face ID)
|
|
13
|
-
- **
|
|
14
|
+
- **Tailwind CSS** - Modern, utility-first CSS framework (optional, included with `--with-tailwind`)
|
|
15
|
+
- **Zod Validation** - Type-safe form validation with TypeScript-first schemas
|
|
16
|
+
- **CLI Tools** - Scaffold apps with `--with-tailwind`, generate models/controllers/resources
|
|
17
|
+
- **Flash Messages** - Session-based success/error messages for forms
|
|
18
|
+
- **Cookie Parser** - Built-in cookie parsing for JWT and session support
|
|
19
|
+
- **Helmet Security** - HTTP header security middleware
|
|
20
|
+
- **CORS Support** - Cross-origin resource sharing middleware
|
|
21
|
+
- **GitHub Actions CI** - Automated testing with optional CI setup
|
|
14
22
|
|
|
15
23
|
## Quick Start
|
|
16
24
|
|
|
@@ -22,581 +30,268 @@ npm run dev
|
|
|
22
30
|
|
|
23
31
|
Visit http://localhost:3000 - your app is running!
|
|
24
32
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
## Getting Started
|
|
28
|
-
|
|
29
|
-
### Step 1: Create a New Page
|
|
30
|
-
|
|
31
|
-
Create `src/views/about.ejs`:
|
|
32
|
-
|
|
33
|
-
```html
|
|
34
|
-
<!doctype html>
|
|
35
|
-
<html>
|
|
36
|
-
<head>
|
|
37
|
-
<title><%= title %></title>
|
|
38
|
-
</head>
|
|
39
|
-
<body>
|
|
40
|
-
<h1><%= title %></h1>
|
|
41
|
-
<p>Welcome to my about page!</p>
|
|
42
|
-
</body>
|
|
43
|
-
</html>
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
### Step 2: Add a Route
|
|
47
|
-
|
|
48
|
-
Edit `src/server.ts`:
|
|
49
|
-
|
|
50
|
-
```typescript
|
|
51
|
-
app.get("/about", (req, res) => {
|
|
52
|
-
res.render("about", { title: "About Us" });
|
|
53
|
-
});
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
### Step 3: View Your Page
|
|
57
|
-
|
|
58
|
-
Visit http://localhost:3000/about
|
|
59
|
-
|
|
60
|
-
---
|
|
61
|
-
|
|
62
|
-
## Creating Pages
|
|
63
|
-
|
|
64
|
-
### EJS Templates
|
|
65
|
-
|
|
66
|
-
Create `.ejs` files in `src/views/`. EJS lets you use JavaScript in HTML:
|
|
67
|
-
|
|
68
|
-
```html
|
|
69
|
-
<!-- Output a variable (escaped) -->
|
|
70
|
-
<h1><%= title %></h1>
|
|
71
|
-
|
|
72
|
-
<!-- Output raw HTML -->
|
|
73
|
-
<%- htmlContent %>
|
|
74
|
-
|
|
75
|
-
<!-- JavaScript logic -->
|
|
76
|
-
<% if (user) { %>
|
|
77
|
-
<p>Welcome, <%= user.name %>!</p>
|
|
78
|
-
<% } %>
|
|
79
|
-
|
|
80
|
-
<!-- Loop through items -->
|
|
81
|
-
<ul>
|
|
82
|
-
<% items.forEach(item => { %>
|
|
83
|
-
<li><%= item.name %></li>
|
|
84
|
-
<% }); %>
|
|
85
|
-
</ul>
|
|
86
|
-
|
|
87
|
-
<!-- Include another template -->
|
|
88
|
-
<%- include('partials/header') %>
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
### Adding Routes
|
|
92
|
-
|
|
93
|
-
```typescript
|
|
94
|
-
// Simple page
|
|
95
|
-
app.get("/contact", (req, res) => {
|
|
96
|
-
res.render("contact", { title: "Contact Us" });
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
// Handle form submission
|
|
100
|
-
app.post("/contact", (req, res) => {
|
|
101
|
-
const { name, email, message } = req.body;
|
|
102
|
-
console.log(`Message from ${name}: ${message}`);
|
|
103
|
-
res.redirect("/contact?sent=true");
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
// JSON API endpoint
|
|
107
|
-
app.get("/api/users", (req, res) => {
|
|
108
|
-
res.json([{ id: 1, name: "John" }]);
|
|
109
|
-
});
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
---
|
|
113
|
-
|
|
114
|
-
## Resources
|
|
33
|
+
## CLI Commands
|
|
115
34
|
|
|
116
|
-
|
|
35
|
+
### Initialize a new app
|
|
117
36
|
|
|
118
37
|
```bash
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
This creates:
|
|
123
|
-
- `prisma/schema.prisma` - Adds the Post model
|
|
124
|
-
- `src/controllers/PostController.ts` - Full CRUD controller with form handling
|
|
125
|
-
- `src/views/posts/index.ejs` - List view
|
|
126
|
-
- `src/views/posts/show.ejs` - Detail view
|
|
127
|
-
- `src/views/posts/create.ejs` - Create form
|
|
128
|
-
- `src/views/posts/edit.ejs` - Edit form
|
|
38
|
+
# Basic app
|
|
39
|
+
npx @erwininteractive/mvc init myapp
|
|
129
40
|
|
|
130
|
-
|
|
41
|
+
# With Tailwind CSS
|
|
42
|
+
npx @erwininteractive/mvc init myapp --with-tailwind
|
|
131
43
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
| `index` | GET | /posts | List all |
|
|
135
|
-
| `create` | GET | /posts/create | Show create form |
|
|
136
|
-
| `store` | POST | /posts | Create new |
|
|
137
|
-
| `show` | GET | /posts/:id | Show one |
|
|
138
|
-
| `edit` | GET | /posts/:id/edit | Show edit form |
|
|
139
|
-
| `update` | PUT | /posts/:id | Update |
|
|
140
|
-
| `destroy` | DELETE | /posts/:id | Delete |
|
|
44
|
+
# With database support (Prisma)
|
|
45
|
+
npx @erwininteractive/mvc init myapp --with-database
|
|
141
46
|
|
|
142
|
-
|
|
47
|
+
# With database + Tailwind
|
|
48
|
+
npx @erwininteractive/mvc init myapp --with-database --with-tailwind
|
|
143
49
|
|
|
144
|
-
|
|
50
|
+
# With GitHub Actions CI
|
|
51
|
+
npx @erwininteractive/mvc init myapp --with-ci
|
|
145
52
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
app.get("/posts", PostController.index);
|
|
150
|
-
app.get("/posts/create", PostController.create);
|
|
151
|
-
app.post("/posts", PostController.store);
|
|
152
|
-
app.get("/posts/:id", PostController.show);
|
|
153
|
-
app.get("/posts/:id/edit", PostController.edit);
|
|
154
|
-
app.put("/posts/:id", PostController.update);
|
|
155
|
-
app.delete("/posts/:id", PostController.destroy);
|
|
53
|
+
# Skip npm install (install manually later)
|
|
54
|
+
npx @erwininteractive/mvc init myapp --skip-install
|
|
156
55
|
```
|
|
157
56
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
## Controllers
|
|
161
|
-
|
|
162
|
-
Generate just a controller (without model/views):
|
|
57
|
+
### Generate models
|
|
163
58
|
|
|
164
59
|
```bash
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
This creates `src/controllers/ProductController.ts` with CRUD actions:
|
|
169
|
-
|
|
170
|
-
| Action | HTTP Method | URL | Description |
|
|
171
|
-
|-----------|-------------|------------------|-------------|
|
|
172
|
-
| `index` | GET | /products | List all |
|
|
173
|
-
| `show` | GET | /products/:id | Show one |
|
|
174
|
-
| `store` | POST | /products | Create |
|
|
175
|
-
| `update` | PUT | /products/:id | Update |
|
|
176
|
-
| `destroy` | DELETE | /products/:id | Delete |
|
|
177
|
-
|
|
178
|
-
### Using Controllers
|
|
179
|
-
|
|
180
|
-
```typescript
|
|
181
|
-
import * as ProductController from "./controllers/ProductController";
|
|
60
|
+
# Generate a Prisma model
|
|
61
|
+
npx erwinmvc generate model User
|
|
62
|
+
npx erwinmvc g model User
|
|
182
63
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
app.post("/products", ProductController.store);
|
|
186
|
-
app.put("/products/:id", ProductController.update);
|
|
187
|
-
app.delete("/products/:id", ProductController.destroy);
|
|
64
|
+
# Generate without running migration
|
|
65
|
+
npx erwinmvc generate model User --skip-migrate
|
|
188
66
|
```
|
|
189
67
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
## Database (Optional)
|
|
193
|
-
|
|
194
|
-
Your app works without a database. Add one when you need it.
|
|
195
|
-
|
|
196
|
-
### Setup
|
|
68
|
+
### Generate controllers
|
|
197
69
|
|
|
198
70
|
```bash
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
Edit `.env` with your database URL:
|
|
203
|
-
|
|
204
|
-
```
|
|
205
|
-
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
Run migrations:
|
|
71
|
+
# Generate a CRUD controller
|
|
72
|
+
npx erwinmvc generate controller User
|
|
73
|
+
npx erwinmvc g controller User
|
|
209
74
|
|
|
210
|
-
|
|
211
|
-
npx
|
|
75
|
+
# Generate without views
|
|
76
|
+
npx erwinmvc generate controller User --no-views
|
|
212
77
|
```
|
|
213
78
|
|
|
214
|
-
### Generate
|
|
79
|
+
### Generate resources (model + controller + views)
|
|
215
80
|
|
|
216
81
|
```bash
|
|
217
|
-
|
|
218
|
-
|
|
82
|
+
# Complete resource with all features
|
|
83
|
+
npx erwinmvc generate resource Post
|
|
84
|
+
npx erwinmvc g resource Post
|
|
219
85
|
|
|
220
|
-
|
|
86
|
+
# Skip specific parts
|
|
87
|
+
npx erwinmvc generate resource Post --skip-model
|
|
88
|
+
npx erwinmvc generate resource Post --skip-views
|
|
89
|
+
npx erwinmvc generate resource Post --skip-controller
|
|
221
90
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
id Int @id @default(autoincrement())
|
|
225
|
-
title String
|
|
226
|
-
content String?
|
|
227
|
-
published Boolean @default(false)
|
|
228
|
-
createdAt DateTime @default(now())
|
|
229
|
-
updatedAt DateTime @updatedAt
|
|
91
|
+
# Skip database migration
|
|
92
|
+
npx erwinmvc generate resource Post --skip-migrate
|
|
230
93
|
|
|
231
|
-
|
|
232
|
-
|
|
94
|
+
# API-only resource (no views, JSON responses)
|
|
95
|
+
npx erwinmvc generate resource Post --api-only
|
|
233
96
|
```
|
|
234
97
|
|
|
235
|
-
|
|
98
|
+
### Authentication commands
|
|
236
99
|
|
|
237
100
|
```bash
|
|
238
|
-
|
|
239
|
-
|
|
101
|
+
# Generate complete authentication system (login/register)
|
|
102
|
+
npx erwinmvc make:auth
|
|
103
|
+
npx erwinmvc ma
|
|
240
104
|
|
|
241
|
-
|
|
105
|
+
# Skip User model (use existing)
|
|
106
|
+
npx erwinmvc make:auth --without-model
|
|
242
107
|
|
|
243
|
-
|
|
244
|
-
|
|
108
|
+
# Only JWT (no sessions)
|
|
109
|
+
npx erwinmvc make:auth --jwt-only
|
|
245
110
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
app.get("/posts", async (req, res) => {
|
|
249
|
-
const posts = await prisma.post.findMany();
|
|
250
|
-
res.render("posts/index", { posts });
|
|
251
|
-
});
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
---
|
|
255
|
-
|
|
256
|
-
## Authentication
|
|
257
|
-
|
|
258
|
-
```typescript
|
|
259
|
-
import {
|
|
260
|
-
hashPassword,
|
|
261
|
-
verifyPassword,
|
|
262
|
-
signToken,
|
|
263
|
-
verifyToken,
|
|
264
|
-
authenticate,
|
|
265
|
-
} from "@erwininteractive/mvc";
|
|
266
|
-
|
|
267
|
-
// Hash a password
|
|
268
|
-
const hash = await hashPassword("secret123");
|
|
269
|
-
|
|
270
|
-
// Verify a password
|
|
271
|
-
const isValid = await verifyPassword("secret123", hash);
|
|
272
|
-
|
|
273
|
-
// Sign a JWT
|
|
274
|
-
const token = signToken({ userId: 1, email: "user@example.com" });
|
|
275
|
-
|
|
276
|
-
// Protect routes with middleware
|
|
277
|
-
app.get("/protected", authenticate, (req, res) => {
|
|
278
|
-
res.json({ user: req.user });
|
|
279
|
-
});
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
### Auto-Inject User to Views
|
|
283
|
-
|
|
284
|
-
When a user is authenticated, their information is automatically available in all EJS templates via `res.locals.user`:
|
|
285
|
-
|
|
286
|
-
```typescript
|
|
287
|
-
// In any EJS template
|
|
288
|
-
<% if (user) { %>
|
|
289
|
-
<p>Welcome, <%= user.email %></p>
|
|
290
|
-
<% } else { %>
|
|
291
|
-
<a href="/auth/login">Login</a>
|
|
292
|
-
<% } %>
|
|
111
|
+
# Only sessions (no JWT)
|
|
112
|
+
npx erwinmvc make:auth --session-only
|
|
293
113
|
```
|
|
294
114
|
|
|
295
|
-
The middleware automatically:
|
|
296
|
-
- Extracts JWT from `req.cookies.token` or `Authorization` header
|
|
297
|
-
- Verifies the token using `JWT_SECRET`
|
|
298
|
-
- Sets `req.user` and `res.locals.user` for use in views
|
|
299
|
-
|
|
300
|
-
---
|
|
301
|
-
|
|
302
115
|
### WebAuthn (Passkeys)
|
|
303
116
|
|
|
304
|
-
Passwordless authentication with security keys:
|
|
305
|
-
|
|
306
117
|
```bash
|
|
118
|
+
# Generate WebAuthn authentication (security key login)
|
|
307
119
|
npx erwinmvc webauthn
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
This generates:
|
|
311
|
-
- `src/controllers/WebAuthnController.ts` - Registration and login handlers
|
|
312
|
-
- `src/views/webauthn/` - EJS views for register/login pages
|
|
313
|
-
- Prisma `WebAuthnCredential` model for storing security key data
|
|
314
|
-
|
|
315
|
-
```typescript
|
|
316
|
-
import {
|
|
317
|
-
startRegistration,
|
|
318
|
-
completeRegistration,
|
|
319
|
-
startAuthentication,
|
|
320
|
-
completeAuthentication,
|
|
321
|
-
getRPConfig,
|
|
322
|
-
} from "@erwininteractive/mvc";
|
|
323
|
-
|
|
324
|
-
const { rpID, rpName } = getRPConfig();
|
|
120
|
+
npx erwinmvc w
|
|
325
121
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const options = await startAuthentication(req, res);
|
|
330
|
-
await completeAuthentication(req, res);
|
|
122
|
+
# Skip database migration
|
|
123
|
+
npx erwinmvc webauthn --skip-migrate
|
|
331
124
|
```
|
|
332
125
|
|
|
333
|
-
|
|
334
|
-
- `WEBAUTHN_RP_ID` - Your domain (e.g., "localhost" or "example.com")
|
|
335
|
-
- `WEBAUTHN_RP_NAME` - Your app name (e.g., "ErwinMVC App")
|
|
336
|
-
|
|
337
|
-
Note: WebAuthn requires HTTPS or localhost.
|
|
338
|
-
|
|
339
|
-
---
|
|
340
|
-
|
|
341
|
-
## CLI Commands
|
|
342
|
-
|
|
343
|
-
| Command | Description |
|
|
344
|
-
|---------|-------------|
|
|
345
|
-
| `npx @erwininteractive/mvc init <dir>` | Create a new app |
|
|
346
|
-
| `npx erwinmvc generate resource <name>` | Generate model + controller + views |
|
|
347
|
-
| `npx erwinmvc generate controller <name>` | Generate a CRUD controller |
|
|
348
|
-
| `npx erwinmvc generate model <name>` | Generate a database model |
|
|
349
|
-
| `npx erwinmvc webauthn` | Generate WebAuthn authentication (security keys) |
|
|
350
|
-
|
|
351
|
-
### Init Options
|
|
352
|
-
|
|
353
|
-
| Option | Description |
|
|
354
|
-
|--------|-------------|
|
|
355
|
-
| `--skip-install` | Skip running npm install |
|
|
356
|
-
| `--with-database` | Include Prisma database setup |
|
|
357
|
-
| `--with-ci` | Include GitHub Actions CI workflow |
|
|
358
|
-
|
|
359
|
-
### Resource Options
|
|
360
|
-
|
|
361
|
-
| Option | Description |
|
|
362
|
-
|--------|-------------|
|
|
363
|
-
| `--skip-model` | Skip generating Prisma model |
|
|
364
|
-
| `--skip-controller` | Skip generating controller |
|
|
365
|
-
| `--skip-views` | Skip generating views |
|
|
366
|
-
| `--skip-migrate` | Skip running Prisma migrate |
|
|
367
|
-
| `--api-only` | Generate API-only controller (no views) |
|
|
126
|
+
### List routes
|
|
368
127
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
| `--no-views` | Skip generating EJS views (controller) |
|
|
375
|
-
|
|
376
|
-
---
|
|
128
|
+
```bash
|
|
129
|
+
# Show all defined routes
|
|
130
|
+
npx erwinmvc list:routes
|
|
131
|
+
npx erwinmvc lr
|
|
132
|
+
```
|
|
377
133
|
|
|
378
134
|
## Project Structure
|
|
379
135
|
|
|
380
136
|
```
|
|
381
137
|
myapp/
|
|
382
138
|
├── src/
|
|
383
|
-
│ ├──
|
|
384
|
-
│ ├── views/
|
|
385
|
-
│
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
├── prisma/
|
|
389
|
-
│ └── schema.prisma
|
|
390
|
-
├── .
|
|
391
|
-
├── .gitignore
|
|
139
|
+
│ ├── controllers/ # MVC controllers
|
|
140
|
+
│ ├── views/ # EJS templates
|
|
141
|
+
│ └── server.ts # App entry point
|
|
142
|
+
├── public/ # Static assets
|
|
143
|
+
│ └── dist/ # Compiled CSS (Tailwind)
|
|
144
|
+
├── prisma/ # Database (optional)
|
|
145
|
+
│ └── schema.prisma # Prisma schema
|
|
146
|
+
├── .github/ # CI workflows (optional)
|
|
392
147
|
├── package.json
|
|
393
|
-
|
|
148
|
+
├── tsconfig.json
|
|
149
|
+
├── tailwind.config.ts # Tailwind config (optional)
|
|
150
|
+
└── postcss.config.cjs # PostCSS config (optional)
|
|
394
151
|
```
|
|
395
152
|
|
|
396
|
-
|
|
153
|
+
## Database Setup
|
|
397
154
|
|
|
398
|
-
|
|
155
|
+
```bash
|
|
156
|
+
# Setup Prisma database
|
|
157
|
+
npx erwinmvc init myapp --with-database
|
|
158
|
+
cd myapp
|
|
399
159
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
160
|
+
# Edit .env with your DATABASE_URL
|
|
161
|
+
# DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
|
|
162
|
+
|
|
163
|
+
# Run migrations
|
|
164
|
+
npx prisma migrate dev --name init
|
|
165
|
+
|
|
166
|
+
# Generate Prisma client (auto-run by CLI)
|
|
167
|
+
npx prisma generate
|
|
403
168
|
```
|
|
404
169
|
|
|
405
|
-
|
|
170
|
+
## Tailwind CSS Setup
|
|
406
171
|
|
|
407
|
-
|
|
172
|
+
```bash
|
|
173
|
+
npx erwinmvc init myapp --with-tailwind
|
|
174
|
+
cd myapp
|
|
408
175
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
| `npm run dev` | Start development server (auto-reload) |
|
|
412
|
-
| `npm run build` | Build for production |
|
|
413
|
-
| `npm start` | Run production build |
|
|
414
|
-
| `npm run db:setup` | Install database dependencies |
|
|
415
|
-
| `npm run db:migrate` | Run database migrations |
|
|
176
|
+
# Configure content paths in tailwind.config.ts
|
|
177
|
+
# Edit src/assets/tailwind.css for custom styles
|
|
416
178
|
|
|
417
|
-
|
|
179
|
+
# Build CSS
|
|
180
|
+
npx tailwindcss -i ./src/assets/tailwind.css -o ./public/dist/tailwind.css --watch
|
|
181
|
+
```
|
|
418
182
|
|
|
419
|
-
##
|
|
183
|
+
## Authentication
|
|
420
184
|
|
|
421
|
-
|
|
185
|
+
### Session + JWT Auth
|
|
422
186
|
|
|
423
|
-
|
|
424
|
-
npx @erwininteractive/mvc init myapp --with-ci
|
|
425
|
-
```
|
|
187
|
+
The `make:auth` command generates:
|
|
426
188
|
|
|
427
|
-
|
|
189
|
+
- User model with password hashing
|
|
190
|
+
- Register/login forms with Zod validation
|
|
191
|
+
- Session-based authentication
|
|
192
|
+
- JWT token generation for API access
|
|
193
|
+
- Password verification with bcrypt
|
|
194
|
+
- Flash messages for errors/success
|
|
428
195
|
|
|
429
|
-
|
|
430
|
-
name: Test
|
|
196
|
+
### WebAuthn (Passkeys)
|
|
431
197
|
|
|
432
|
-
|
|
433
|
-
push:
|
|
434
|
-
branches: [main]
|
|
435
|
-
pull_request:
|
|
436
|
-
branches: [main]
|
|
198
|
+
The `webauthn` command generates:
|
|
437
199
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
200
|
+
- Passkey registration flow
|
|
201
|
+
- Passkey authentication flow
|
|
202
|
+
- User credential storage in database
|
|
203
|
+
- Security key support (YubiKey, Touch ID, Face ID)
|
|
441
204
|
|
|
442
|
-
|
|
443
|
-
- name: Checkout
|
|
444
|
-
uses: actions/checkout@v4
|
|
205
|
+
## Validation
|
|
445
206
|
|
|
446
|
-
|
|
447
|
-
uses: actions/setup-node@v4
|
|
448
|
-
with:
|
|
449
|
-
node-version: "20"
|
|
450
|
-
cache: "npm"
|
|
207
|
+
Use Zod schemas for type-safe form validation:
|
|
451
208
|
|
|
452
|
-
|
|
453
|
-
|
|
209
|
+
```typescript
|
|
210
|
+
import { z } from "zod";
|
|
211
|
+
import { validate } from "@erwininteractive/mvc";
|
|
454
212
|
|
|
455
|
-
|
|
456
|
-
|
|
213
|
+
const userSchema = z.object({
|
|
214
|
+
username: z.string().min(3),
|
|
215
|
+
email: z.string().email(),
|
|
216
|
+
password: z.string().min(8)
|
|
217
|
+
});
|
|
457
218
|
|
|
458
|
-
|
|
459
|
-
|
|
219
|
+
app.post("/users", validate(userSchema), async (req, res) => {
|
|
220
|
+
const user = req.validatedBody; // Type-safe validated data
|
|
221
|
+
// ...
|
|
222
|
+
});
|
|
460
223
|
```
|
|
461
224
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
If your app uses a database, add PostgreSQL as a service:
|
|
465
|
-
|
|
466
|
-
```yaml
|
|
467
|
-
jobs:
|
|
468
|
-
test:
|
|
469
|
-
runs-on: ubuntu-latest
|
|
470
|
-
|
|
471
|
-
services:
|
|
472
|
-
postgres:
|
|
473
|
-
image: postgres:16
|
|
474
|
-
env:
|
|
475
|
-
POSTGRES_USER: postgres
|
|
476
|
-
POSTGRES_PASSWORD: postgres
|
|
477
|
-
POSTGRES_DB: test
|
|
478
|
-
ports:
|
|
479
|
-
- 5432:5432
|
|
480
|
-
options: >-
|
|
481
|
-
--health-cmd pg_isready
|
|
482
|
-
--health-interval 10s
|
|
483
|
-
--health-timeout 5s
|
|
484
|
-
--health-retries 5
|
|
485
|
-
|
|
486
|
-
steps:
|
|
487
|
-
- name: Checkout
|
|
488
|
-
uses: actions/checkout@v4
|
|
489
|
-
|
|
490
|
-
- name: Setup Node.js
|
|
491
|
-
uses: actions/setup-node@v4
|
|
492
|
-
with:
|
|
493
|
-
node-version: "20"
|
|
494
|
-
cache: "npm"
|
|
495
|
-
|
|
496
|
-
- name: Install dependencies
|
|
497
|
-
run: npm ci
|
|
498
|
-
|
|
499
|
-
- name: Run migrations
|
|
500
|
-
run: npx prisma migrate deploy
|
|
501
|
-
env:
|
|
502
|
-
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
|
|
503
|
-
|
|
504
|
-
- name: Run tests
|
|
505
|
-
run: npm test
|
|
506
|
-
env:
|
|
507
|
-
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
|
|
508
|
-
|
|
509
|
-
- name: Build
|
|
510
|
-
run: npm run build
|
|
511
|
-
```
|
|
225
|
+
## API Reference
|
|
512
226
|
|
|
513
|
-
###
|
|
227
|
+
### Core Functions
|
|
514
228
|
|
|
515
|
-
|
|
229
|
+
- `createMvcApp(options)` - Create Express app with views, static files
|
|
230
|
+
- `startServer(app)` - Start HTTP server on port 3000
|
|
231
|
+
- `hashPassword(plain)` - Hash password with bcrypt
|
|
232
|
+
- `verifyPassword(plain, hash)` - Verify password
|
|
233
|
+
- `signToken(payload, expiresIn)` - Sign JWT token
|
|
234
|
+
- `verifyToken(token)` - Verify and decode JWT
|
|
235
|
+
- `authenticate` - Express middleware for JWT authentication
|
|
236
|
+
- `validate(schema, strategy)` - Zod validation middleware
|
|
516
237
|
|
|
517
|
-
|
|
518
|
-
|--------|-------------|
|
|
519
|
-
| `DATABASE_URL` | Production database connection string |
|
|
520
|
-
| `REDIS_URL` | Production Redis connection string |
|
|
521
|
-
| `JWT_SECRET` | Secret key for JWT signing |
|
|
522
|
-
| `SESSION_SECRET` | Secret key for session encryption |
|
|
238
|
+
### WebAuthn Functions
|
|
523
239
|
|
|
524
|
-
|
|
240
|
+
- `startRegistration(req, res)` - Begin WebAuthn registration
|
|
241
|
+
- `completeRegistration(req, res)` - Complete WebAuthn registration
|
|
242
|
+
- `startAuthentication(req, res)` - Begin WebAuthn login
|
|
243
|
+
- `completeAuthentication(req, res)` - Complete WebAuthn login
|
|
244
|
+
- `getRPConfig()` - Get relying party configuration
|
|
525
245
|
|
|
526
|
-
|
|
527
|
-
env:
|
|
528
|
-
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
|
529
|
-
JWT_SECRET: ${{ secrets.JWT_SECRET }}
|
|
530
|
-
```
|
|
246
|
+
### Validation Helpers
|
|
531
247
|
|
|
532
|
-
|
|
248
|
+
- `getFieldErrors(error)` - Extract field errors from Zod error
|
|
249
|
+
- `getOldInput(req)` - Get previously submitted form data
|
|
250
|
+
- `getErrors(req)` - Get flash error messages
|
|
251
|
+
- `hasFieldError(field, errors)` - Check if field has errors
|
|
252
|
+
- `getFieldError(field, errors)` - Get error message for field
|
|
533
253
|
|
|
534
254
|
## Environment Variables
|
|
535
255
|
|
|
536
|
-
|
|
256
|
+
Required for full functionality:
|
|
537
257
|
|
|
538
258
|
```env
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
JWT_SECRET="your-secret-key" # For auth
|
|
542
|
-
SESSION_SECRET="your-session-secret" # For sessions
|
|
543
|
-
PORT=3000 # Server port
|
|
544
|
-
NODE_ENV=development # Environment
|
|
545
|
-
```
|
|
259
|
+
# Required
|
|
260
|
+
JWT_SECRET=your-secret-key-here
|
|
546
261
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
Use EJS for layout while adding React components where needed:
|
|
554
|
-
|
|
555
|
-
```html
|
|
556
|
-
<!-- layout.ejs -->
|
|
557
|
-
<!DOCTYPE html>
|
|
558
|
-
<html>
|
|
559
|
-
<head><title><%= title %></title></head>
|
|
560
|
-
<body>
|
|
561
|
-
<header><%- include('partial/header') %></header>
|
|
562
|
-
|
|
563
|
-
<main id="app"><%- body %></main>
|
|
564
|
-
|
|
565
|
-
<!-- React island -->
|
|
566
|
-
<div id="comment-section" data-post-id="<%= post.id %>"></div>
|
|
567
|
-
|
|
568
|
-
<footer><%- include('partial/footer') %></footer>
|
|
569
|
-
|
|
570
|
-
<script type="module" src="/src/components/CommentSection.tsx"></script>
|
|
571
|
-
</body>
|
|
572
|
-
</html>
|
|
262
|
+
# Database (optional)
|
|
263
|
+
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
|
|
264
|
+
|
|
265
|
+
# WebAuthn (optional, defaults to localhost)
|
|
266
|
+
WEBAUTHN_RP_ID=localhost
|
|
267
|
+
WEBAUTHN_RP_NAME=MyApp
|
|
573
268
|
```
|
|
574
269
|
|
|
575
|
-
|
|
270
|
+
## Testing
|
|
576
271
|
|
|
577
|
-
|
|
272
|
+
```bash
|
|
273
|
+
# Run all tests
|
|
274
|
+
npm test
|
|
578
275
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
});
|
|
276
|
+
# Run specific test file
|
|
277
|
+
npm test -- auth
|
|
278
|
+
npm test -- cli
|
|
279
|
+
npm test -- generators
|
|
584
280
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
res.json(users); // Always JSON, no EJS
|
|
588
|
-
});
|
|
281
|
+
# With coverage
|
|
282
|
+
npm run test -- --coverage
|
|
589
283
|
```
|
|
590
284
|
|
|
591
|
-
|
|
285
|
+
## Production Build
|
|
592
286
|
|
|
593
|
-
|
|
287
|
+
```bash
|
|
288
|
+
# Build TypeScript and CLI
|
|
289
|
+
npm run build
|
|
594
290
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
- [Alpine.js Documentation](https://alpinejs.dev/)
|
|
291
|
+
# The app is ready for production deployment
|
|
292
|
+
# Server runs on port 3000
|
|
293
|
+
```
|
|
599
294
|
|
|
600
295
|
## License
|
|
601
296
|
|
|
602
|
-
MIT
|
|
297
|
+
MIT © [Erwin Interactive](https://github.com/erwininteractive)
|
|
@@ -131,48 +131,38 @@ function setupDatabase(targetDir) {
|
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
/**
|
|
134
|
-
* Setup Tailwind CSS with
|
|
134
|
+
* Setup Tailwind CSS with pre-built CSS.
|
|
135
135
|
*/
|
|
136
136
|
function setupTailwind(targetDir) {
|
|
137
137
|
console.log("\nSetting up Tailwind CSS...");
|
|
138
138
|
try {
|
|
139
139
|
(0, child_process_1.execSync)("npm install -D tailwindcss postcss autoprefixer", { cwd: targetDir, stdio: "inherit" });
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
const templateDir = (0, paths_1.getTemplatesDir)();
|
|
141
|
+
const templateCss = path_1.default.join(templateDir, "appScaffold", "public", "dist", "tailwind.css");
|
|
142
|
+
const targetCss = path_1.default.join(targetDir, "public", "dist", "tailwind.css");
|
|
143
|
+
fs_1.default.mkdirSync(path_1.default.dirname(targetCss), { recursive: true });
|
|
144
|
+
fs_1.default.copyFileSync(templateCss, targetCss);
|
|
142
145
|
const assetsDir = path_1.default.join(targetDir, "src", "assets");
|
|
143
146
|
fs_1.default.mkdirSync(assetsDir, { recursive: true });
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
@tailwind components;
|
|
147
|
-
@tailwind utilities;
|
|
148
|
-
`;
|
|
149
|
-
fs_1.default.writeFileSync(path_1.default.join(assetsDir, "tailwind.css"), tailwindCss);
|
|
150
|
-
// Update postcss.config.cjs
|
|
151
|
-
const postcssConfig = `/** @type {import('postcss-load-config').Config} */
|
|
152
|
-
const config = {
|
|
153
|
-
plugins: {
|
|
154
|
-
tailwindcss: {},
|
|
155
|
-
autoprefixer: {},
|
|
156
|
-
},
|
|
157
|
-
};
|
|
147
|
+
fs_1.default.writeFileSync(path_1.default.join(assetsDir, "tailwind.css"), `@import "tailwindcss";\n`);
|
|
148
|
+
const config = `import { type Config } from "tailwindcss";
|
|
158
149
|
|
|
159
|
-
export default
|
|
150
|
+
export default {
|
|
151
|
+
content: ["./src/**/*.{js,ts,tsx}"],
|
|
152
|
+
theme: { extend: {} },
|
|
153
|
+
plugins: [],
|
|
154
|
+
} satisfies Config;
|
|
160
155
|
`;
|
|
161
|
-
fs_1.default.writeFileSync(path_1.default.join(targetDir, "
|
|
162
|
-
|
|
163
|
-
const packageJsonPath = path_1.default.join(targetDir, "package.json");
|
|
164
|
-
if (fs_1.default.existsSync(packageJsonPath)) {
|
|
165
|
-
const pkg = JSON.parse(fs_1.default.readFileSync(packageJsonPath, "utf-8"));
|
|
166
|
-
pkg.scripts.tailwind = "tailwindcss -i ./src/assets/tailwind.css -o ./public/dist/tailwind.css --watch";
|
|
167
|
-
pkg.scripts.build = "tsc && npm run tailwind";
|
|
168
|
-
fs_1.default.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2));
|
|
169
|
-
console.log("✓ Added Tailwind CSS support");
|
|
170
|
-
}
|
|
156
|
+
fs_1.default.writeFileSync(path_1.default.join(targetDir, "tailwind.config.ts"), config);
|
|
157
|
+
console.log("✓ Added Tailwind CSS support");
|
|
171
158
|
}
|
|
172
159
|
catch {
|
|
173
|
-
console.error("Failed to setup Tailwind CSS.
|
|
160
|
+
console.error("Failed to setup Tailwind CSS.");
|
|
174
161
|
}
|
|
175
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Recursively copy a directory.
|
|
165
|
+
*/
|
|
176
166
|
/**
|
|
177
167
|
* Recursively copy a directory.
|
|
178
168
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@erwininteractive/mvc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "A lightweight, full-featured MVC framework for Node.js with Express, Prisma, and EJS",
|
|
5
5
|
"main": "dist/framework/index.js",
|
|
6
6
|
"types": "dist/framework/index.d.ts",
|
|
@@ -51,8 +51,10 @@
|
|
|
51
51
|
"express-session": "^1.18.0",
|
|
52
52
|
"helmet": "^8.0.0",
|
|
53
53
|
"jsonwebtoken": "^9.0.2",
|
|
54
|
+
"postcss": "^8.4.38",
|
|
54
55
|
"prisma": "^6.0.0",
|
|
55
56
|
"redis": "^4.7.0",
|
|
57
|
+
"tailwindcss": "^4.2.2",
|
|
56
58
|
"zod": "^3.23.8"
|
|
57
59
|
},
|
|
58
60
|
"devDependencies": {
|