webspresso 0.0.98 → 0.0.100
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/.agents/ADMIN.md +11 -1
- package/.agents/AGENTS.md +2 -2
- package/.agents/AUTH.md +415 -0
- package/.agents/CLI.md +47 -0
- package/.agents/ERRORS.md +59 -0
- package/.agents/ORM.md +178 -4
- package/.agents/PLUGINS.md +113 -8
- package/.agents/QUEUE.md +92 -0
- package/.agents/ROUTING.md +160 -7
- package/.agents/STREAMING.md +35 -0
- package/AGENTS.md +2 -0
- package/README.md +145 -93
- package/bin/commands/add-docker.js +467 -0
- package/bin/commands/deploy-check.js +339 -0
- package/bin/webspresso.js +4 -0
- package/core/auth/index.js +123 -1
- package/core/auth/magic-link.js +170 -0
- package/core/auth/manager.js +477 -3
- package/core/auth/middleware.js +118 -5
- package/core/auth/oauth.js +517 -0
- package/core/auth/qrcode.js +499 -0
- package/core/auth/session-manager.js +255 -0
- package/core/auth/sessions.js +351 -0
- package/core/auth/tokens.js +1 -0
- package/core/auth/totp.js +507 -0
- package/core/auth/user-agent.js +161 -0
- package/core/orm/eager-loader.js +144 -10
- package/core/orm/index.js +10 -1
- package/core/orm/model.js +55 -1
- package/core/orm/query-builder.js +90 -4
- package/core/orm/repository.js +327 -16
- package/core/orm/scopes.js +3 -2
- package/core/orm/search.js +213 -0
- package/core/orm/types.js +6 -1
- package/core/queue/cron.js +544 -0
- package/core/queue/index.js +8 -1
- package/core/queue/manager.js +100 -1
- package/core/queue/scheduler.js +415 -0
- package/core/sse/index.js +19 -0
- package/core/sse/stream.js +430 -0
- package/core/ssr/stream.js +62 -2
- package/index.d.ts +794 -6
- package/index.js +48 -0
- package/package.json +7 -5
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +323 -31
- package/plugins/admin-panel/client/parts/08-record-list.js +5 -0
- package/plugins/admin-panel/client/verify-spa-parts.js +2 -0
- package/plugins/admin-panel/field-renderers/file-upload.js +187 -104
- package/plugins/admin-panel/field-renderers/index.js +2 -0
- package/plugins/admin-panel/modules/admin-users.js +1 -1
- package/plugins/api-key/crypto.js +101 -0
- package/plugins/api-key/index.js +288 -0
- package/plugins/api-key/middleware.js +461 -0
- package/plugins/api-key/migration-template.js +48 -0
- package/plugins/api-key/model.js +50 -0
- package/plugins/api-key/scopes.js +125 -0
- package/plugins/audit-log/admin-component.js +487 -70
- package/plugins/audit-log/api-handlers.js +114 -0
- package/plugins/audit-log/index.js +24 -0
- package/plugins/content/client/inline-edit.js +2 -46
- package/plugins/cors.js +13 -2
- package/plugins/email/auth-bridge.js +66 -0
- package/plugins/email/bundled-templates.js +15 -0
- package/plugins/email/templates/auth-magic-link.mjml +13 -0
- package/plugins/file-manager/admin-component.js +69 -53
- package/plugins/file-manager/index.js +25 -8
- package/plugins/index.js +8 -0
- package/plugins/oauth/index.js +338 -0
- package/plugins/queue/index.js +4 -0
- package/plugins/webhooks/admin-component.js +608 -0
- package/plugins/webhooks/api-handlers.js +237 -0
- package/plugins/webhooks/crypto.js +144 -0
- package/plugins/webhooks/dispatcher.js +708 -0
- package/plugins/webhooks/index.js +263 -0
- package/plugins/webhooks/matcher.js +113 -0
- package/plugins/webhooks/migration-template.js +75 -0
- package/plugins/webhooks/models.js +98 -0
- package/plugins/webhooks/sender.js +176 -0
- package/src/api/api-loader.js +1 -0
- package/src/bundle/index.js +2 -1
- package/src/discovery/file-route-parser.js +44 -1
- package/src/discovery/scan-routes.js +2 -2
- package/src/file-router.js +310 -104
- package/src/helpers.js +16 -1
- package/src/njk-frontmatter.js +3 -0
- package/src/routing/cascading.js +490 -0
- package/src/routing/middleware-resolver.js +92 -0
- package/src/server.js +6 -0
package/.agents/ADMIN.md
CHANGED
|
@@ -225,7 +225,7 @@ registry.registerWidget('recent-orders', {
|
|
|
225
225
|
- Top tables by row count with visual progress percentage bars
|
|
226
226
|
- Direct navigation links to model CRUD screens
|
|
227
227
|
- **`quick-actions`**: Fast shortcuts to create new records across enabled models.
|
|
228
|
-
- **`recent-activity`**: Timeline of recent administrative mutations (creates, updates, deletes).
|
|
228
|
+
- **`recent-activity`**: Timeline of recent administrative mutations (creates, updates, deletes) powered by `audit_logs` / `admin_activity_logs` with direct links to record changes.
|
|
229
229
|
|
|
230
230
|
---
|
|
231
231
|
|
|
@@ -245,6 +245,16 @@ registry.registerAction('Product', {
|
|
|
245
245
|
},
|
|
246
246
|
});
|
|
247
247
|
|
|
248
|
+
// Record action with navigation redirect (e.g. audit-timeline)
|
|
249
|
+
registry.registerAction('Product', {
|
|
250
|
+
id: 'view-history',
|
|
251
|
+
label: 'History',
|
|
252
|
+
icon: 'clock',
|
|
253
|
+
handler: async (record, model) => {
|
|
254
|
+
return { redirect: `/_admin/audit-log?model=${model.name}&recordId=${record.id}` };
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
|
|
248
258
|
// Bulk multi-select table action
|
|
249
259
|
registry.registerBulkAction('Product', {
|
|
250
260
|
id: 'bulk-archive',
|
package/.agents/AGENTS.md
CHANGED
|
@@ -44,11 +44,11 @@ This `.agents/` customization root is organized into modular topic guides:
|
|
|
44
44
|
6. **[HTTP Response Compression](.agents/COMPRESSION.md)** — Native streaming zlib compression (`server.compression`), Brotli/Gzip/Deflate negotiation, threshold handling, and `res.compress(false)` route opt-out.
|
|
45
45
|
7. **[Dual Authentication System](.agents/AUTH.md)** — Stateful Session + Cookie & Stateless HS256 JWT + Refresh Token Rotation, `req.auth` helpers, and `middleware: ['jwt']` guard integrations.
|
|
46
46
|
8. **[ORM & Database Layer](.agents/ORM.md)** — Model definitions with `defineModel`, `zdb` schemas, Repositories API, Query Caching, Relations, Soft Delete, Scopes & Migrations.
|
|
47
|
-
9. **[Plugin Ecosystem Guide](.agents/PLUGINS.md)** — Lifecycle hooks (`register`, `onRoutesReady`), CSP headers, and detailed reference for all
|
|
47
|
+
9. **[Plugin Ecosystem Guide](.agents/PLUGINS.md)** — Lifecycle hooks (`register`, `onRoutesReady`), CSP headers, and detailed reference for all 20 built-in official plugins (including `webhooksPlugin`, `apiKeyPlugin`, `xlsxPlugin`, `csvPlugin`, `fileManagerPlugin`, `realtimePlugin`, and `basicAuthPlugin`).
|
|
48
48
|
10. **[Realtime Layer & Adapters](.agents/REALTIME.md)** — Framework-agnostic realtime layer (`core/realtime`), generic WebSocket, SSE, and Socket.IO adapters, subscription identity, backoff reconnect, and auth lifecycle.
|
|
49
49
|
11. **[Email Plugin Guide](.agents/EMAIL.md)** — MJML template compilation, Nodemailer transport, DB delivery logs, Auth Email bridge, and Admin UI.
|
|
50
50
|
12. **[Admin Panel Customization](.agents/ADMIN.md)** — Extending Mithril.js Admin Panel SPA (Custom pages `component.js`, custom field renderers, widgets, single & bulk actions).
|
|
51
|
-
13. **[CLI Tooling & Commands](
|
|
51
|
+
13. **[CLI Tooling & Commands](CLI.md)** — `webspresso dev`, `build`, `doctor`, `db:migrate`, `db:seed`, `polar:migrate`, `favicon:generate`, `add:tailwind`, `add:docker`, `deploy:check`, `agents:init`.
|
|
52
52
|
14. **[Development & Testing Workflow](.agents/WORKFLOW.md)** — CLI commands, Vitest unit/integration testing, Playwright E2E tests, TypeScript type checking, and zero regression policy.
|
|
53
53
|
15. **[Coding Standards & Conventions](.agents/CONVENTIONS.md)** — Zero external dependencies policy, strict function signature & schema verification, clean component separation, and log traceback inspection rules.
|
|
54
54
|
16. **[Background Job Queue](.agents/QUEUE.md)** — In-Memory, Knex Database, and Distributed Redis queue adapters, job auto-discovery (`jobs/`), retries, and graceful draining.
|
package/.agents/AUTH.md
CHANGED
|
@@ -45,6 +45,10 @@ Attached to Express request during auth middleware execution:
|
|
|
45
45
|
| `req.auth.refreshAccessToken(refreshToken, options)` | Refresh access token & rotate refresh token |
|
|
46
46
|
| `req.auth.check()` / `req.auth.guest()` | Check if user is authenticated or guest |
|
|
47
47
|
| `req.auth.user()` / `req.auth.id()` | Get current user object or ID |
|
|
48
|
+
| `req.auth.sessions()` | List active sessions & device info for current user |
|
|
49
|
+
| `req.auth.revokeSession(sessionId)` | Revoke a specific session (remotely log out device) |
|
|
50
|
+
| `req.auth.revokeOtherSessions()` | Revoke all sessions except the current active session |
|
|
51
|
+
| `req.auth.revokeAllSessions(userId?)` | Revoke all active sessions (force logout from all devices) |
|
|
48
52
|
|
|
49
53
|
---
|
|
50
54
|
|
|
@@ -101,3 +105,414 @@ module.exports = {
|
|
|
101
105
|
},
|
|
102
106
|
};
|
|
103
107
|
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 5. Zero-Dependency OAuth 2.0 & Social Login (`plugins/oauth`, `core/auth/oauth`)
|
|
112
|
+
|
|
113
|
+
Webspresso includes a built-in, zero-dependency OAuth 2.0 client and plugin with RFC 7636 PKCE, HMAC-SHA256 signed anti-CSRF state tokens, profile normalization, and seamless Dual Auth integration:
|
|
114
|
+
|
|
115
|
+
### 5.1 Plugin Configuration
|
|
116
|
+
```javascript
|
|
117
|
+
const { createApp, oauthPlugin } = require('webspresso');
|
|
118
|
+
|
|
119
|
+
const app = createApp({
|
|
120
|
+
plugins: [
|
|
121
|
+
oauthPlugin({
|
|
122
|
+
secret: process.env.SESSION_SECRET,
|
|
123
|
+
providers: {
|
|
124
|
+
google: {
|
|
125
|
+
clientId: process.env.GOOGLE_CLIENT_ID,
|
|
126
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
127
|
+
// redirectUri is optional; auto-inferred from request host if omitted
|
|
128
|
+
},
|
|
129
|
+
github: {
|
|
130
|
+
clientId: process.env.GITHUB_CLIENT_ID,
|
|
131
|
+
clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
// Optional hook to find or create the user in your database
|
|
135
|
+
async findOrCreateUser({ provider, profile, tokens, db }) {
|
|
136
|
+
let user = await db.getRepository('User').findOne({ email: profile.email });
|
|
137
|
+
if (!user) {
|
|
138
|
+
user = await db.getRepository('User').create({
|
|
139
|
+
email: profile.email,
|
|
140
|
+
name: profile.name,
|
|
141
|
+
avatar_url: profile.avatarUrl,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return user;
|
|
145
|
+
},
|
|
146
|
+
defaultReturnTo: '/dashboard',
|
|
147
|
+
}),
|
|
148
|
+
],
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### 5.2 Automatic Routes
|
|
153
|
+
- `GET /auth/:provider` (e.g. `/auth/google?returnTo=/dashboard`): Generates PKCE code challenge and HMAC-signed state token with embedded return URL, redirecting the browser to the provider's authorization page.
|
|
154
|
+
- `GET /auth/:provider/callback`: Validates anti-CSRF state, exchanges authorization code for tokens, fetches normalized profile, calls `findOrCreateUser`, automatically establishes session via `req.auth.login(user)`, and redirects to `returnTo`. For API/mobile clients (`format=json` or `Accept: application/json`), returns `{ success: true, user, token }`.
|
|
155
|
+
|
|
156
|
+
### 5.3 Template Helper (`fsy.oauthUrl`)
|
|
157
|
+
In Nunjucks templates:
|
|
158
|
+
```html
|
|
159
|
+
<a href="{{ fsy.oauthUrl('google') }}" class="btn">Sign in with Google</a>
|
|
160
|
+
<a href="{{ fsy.oauthUrl('github', { returnTo: '/settings' }) }}" class="btn">Connect GitHub</a>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## 6. Zero-Dependency Two-Factor Authentication (2FA / TOTP)
|
|
166
|
+
|
|
167
|
+
Webspresso provides an enterprise-grade, zero-dependency 2FA/TOTP suite adhering strictly to **RFC 6238 (TOTP)**, **RFC 4226 (HOTP)**, and **RFC 4648 (Base32)**. It requires NO external npm libraries and features offline SVG QR code generation to prevent leaking secret keys to third-party APIs.
|
|
168
|
+
|
|
169
|
+
### 6.1 Key Capabilities
|
|
170
|
+
- **RFC 6238 Time-Based OTP**: Standard 30s step, 6/8 digits, HMAC-SHA1/SHA256/SHA512.
|
|
171
|
+
- **Time Drift Tolerance**: Built-in clock drift window (`window: 1` verifies $t \pm 30s$) and detailed delta analysis.
|
|
172
|
+
- **Offline QR Code Generator**: Generates crisp vector SVG strings and data URIs directly in pure Node.js.
|
|
173
|
+
- **Single-Use Recovery Codes**: Cryptographically secure alphanumeric codes, SHA-256 hashed storage, and timing-safe single-use burning.
|
|
174
|
+
- **Session & Guard Integration**: `req.auth.verify2Fa()`, `req.auth.is2FaVerified()`, and `middleware: ['2fa']`.
|
|
175
|
+
|
|
176
|
+
### 6.2 Initializing 2FA Setup for a User
|
|
177
|
+
```javascript
|
|
178
|
+
const { totp } = require('webspresso');
|
|
179
|
+
|
|
180
|
+
// In an API route: POST /api/user/2fa/setup
|
|
181
|
+
module.exports = {
|
|
182
|
+
middleware: ['auth'],
|
|
183
|
+
async handler(req, res) {
|
|
184
|
+
const setup = totp.createTotpSetup({
|
|
185
|
+
user: req.user,
|
|
186
|
+
issuer: 'My SaaS App',
|
|
187
|
+
recoveryCodeCount: 8,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// 1. Temporarily save secret and hashed recovery codes to DB for confirmation
|
|
191
|
+
await req.db.getRepository('User').update(req.user.id, {
|
|
192
|
+
temp_totp_secret: setup.secret,
|
|
193
|
+
temp_recovery_codes: JSON.stringify(setup.recoveryCodesHashed),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// 2. Return QR SVG and plaintext recovery codes for user to record
|
|
197
|
+
return res.json({
|
|
198
|
+
secret: setup.secret,
|
|
199
|
+
qrSvg: setup.qrSvg,
|
|
200
|
+
qrDataUri: setup.qrDataUri,
|
|
201
|
+
recoveryCodes: setup.recoveryCodes,
|
|
202
|
+
});
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### 6.3 Confirming 2FA Activation
|
|
208
|
+
```javascript
|
|
209
|
+
// In an API route: POST /api/user/2fa/confirm
|
|
210
|
+
module.exports = {
|
|
211
|
+
middleware: ['auth'],
|
|
212
|
+
schema: ({ z }) => ({
|
|
213
|
+
body: z.object({ token: z.string().min(6).max(8) }),
|
|
214
|
+
}),
|
|
215
|
+
async handler(req, res) {
|
|
216
|
+
const user = req.user;
|
|
217
|
+
const { token } = req.input.body;
|
|
218
|
+
|
|
219
|
+
const isValid = totp.verifyTotp(token, user.temp_totp_secret);
|
|
220
|
+
if (!isValid) {
|
|
221
|
+
return res.status(400).json({ error: 'Invalid verification code' });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Activate 2FA on user record
|
|
225
|
+
await req.db.getRepository('User').update(user.id, {
|
|
226
|
+
two_factor_enabled: true,
|
|
227
|
+
totp_secret: user.temp_totp_secret,
|
|
228
|
+
recovery_codes: user.temp_recovery_codes,
|
|
229
|
+
temp_totp_secret: null,
|
|
230
|
+
temp_recovery_codes: null,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
req.auth.set2FaVerified(true);
|
|
234
|
+
return res.json({ success: true, message: '2FA enabled successfully' });
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### 6.4 2FA Login Flow & Guard Middleware
|
|
240
|
+
```javascript
|
|
241
|
+
// Step 1: Initial password login
|
|
242
|
+
const user = await req.auth.findUserByCredentials(email, password);
|
|
243
|
+
if (user.two_factor_enabled) {
|
|
244
|
+
// Mark session as 2FA pending without full login
|
|
245
|
+
req.auth.set2FaPending(user);
|
|
246
|
+
return res.json({ require2Fa: true });
|
|
247
|
+
}
|
|
248
|
+
await req.auth.login(user);
|
|
249
|
+
|
|
250
|
+
// Step 2: 2FA Verification Endpoint (accepts 6-digit TOTP token OR recovery code)
|
|
251
|
+
const verification = req.auth.verify2Fa(tokenOrRecoveryCode, user);
|
|
252
|
+
if (verification.success) {
|
|
253
|
+
if (verification.type === 'recovery_code') {
|
|
254
|
+
// Burn used recovery code
|
|
255
|
+
await req.db.getRepository('User').update(user.id, {
|
|
256
|
+
recovery_codes: JSON.stringify(verification.remainingHashedCodes),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
await req.auth.login(user);
|
|
260
|
+
req.auth.set2FaVerified(true);
|
|
261
|
+
return res.json({ success: true });
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### 6.5 Route Protection with `middleware: ['2fa']`
|
|
266
|
+
```javascript
|
|
267
|
+
// In pages/api/account/billing.get.js or pages/security/index.njk
|
|
268
|
+
module.exports = {
|
|
269
|
+
middleware: ['auth', '2fa'], // Rejects unverified users with 403 or redirects to /2fa
|
|
270
|
+
async handler(req, res) {
|
|
271
|
+
return res.json({ secureData: true });
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## 7. Magic Link & Passwordless Authentication
|
|
279
|
+
|
|
280
|
+
Webspresso includes zero-dependency, cryptographically secure **Magic Link / Passwordless Authentication**:
|
|
281
|
+
- **Single-Use Timed Tokens**: 32-byte cryptographically secure random tokens stored hashed (`SHA-256`) in the `auth_tokens` table (`type = 'magic_link'`) with a default 15-minute TTL.
|
|
282
|
+
- **Anti-Enumeration Protection**: `requestMagicLink` returns `{ sent: true, userFound: false }` for unknown emails by default so attackers cannot probe user existence.
|
|
283
|
+
- **Auto-Verification**: Successfully signing in via a magic link automatically marks `email_verified_at` if the user is not yet verified.
|
|
284
|
+
- **Dual Session & JWT Support**: Seamlessly establishes web sessions (`req.auth.login`) and/or issues JWT access/refresh token pairs.
|
|
285
|
+
- **Native Email Plugin Integration**: Built-in `auth-magic-link` MJML template, automated delivery via `plugins/email`'s `wireAuthEmails`, and pre-wired `/api/auth/magic-link` routes.
|
|
286
|
+
|
|
287
|
+
### 7.1 Setup in `quickAuth`
|
|
288
|
+
```javascript
|
|
289
|
+
const { quickAuth } = require('webspresso/core/auth');
|
|
290
|
+
|
|
291
|
+
const auth = quickAuth({
|
|
292
|
+
db,
|
|
293
|
+
userModel: 'User',
|
|
294
|
+
identifierField: 'email',
|
|
295
|
+
magicLink: true, // Automatically provisions Knex auth_tokens adapter
|
|
296
|
+
});
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### 7.2 Requesting a Magic Link (`pages/api/auth/magic-link.post.js`)
|
|
300
|
+
```javascript
|
|
301
|
+
const { defineApi } = require('webspresso');
|
|
302
|
+
|
|
303
|
+
module.exports = defineApi({
|
|
304
|
+
schema: ({ z }) => ({
|
|
305
|
+
body: z.object({
|
|
306
|
+
email: z.string().email(),
|
|
307
|
+
}),
|
|
308
|
+
}),
|
|
309
|
+
async handler(req, res) {
|
|
310
|
+
const { email } = req.input.body;
|
|
311
|
+
|
|
312
|
+
// Dispatches email notification via plugins/email (if active)
|
|
313
|
+
await req.auth.requestMagicLink(email, {
|
|
314
|
+
tokenTtlMs: 15 * 60 * 1000, // 15 mins (default)
|
|
315
|
+
path: '/auth/magic-link/verify',
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
return res.json({
|
|
319
|
+
ok: true,
|
|
320
|
+
message: 'If an account exists with this email, a sign-in link has been sent.',
|
|
321
|
+
});
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### 7.3 Verifying & Logging In (`pages/api/auth/magic-link/verify.get.js`)
|
|
327
|
+
```javascript
|
|
328
|
+
const { defineApi } = require('webspresso');
|
|
329
|
+
|
|
330
|
+
module.exports = defineApi({
|
|
331
|
+
schema: ({ z }) => ({
|
|
332
|
+
query: z.object({
|
|
333
|
+
token: z.string().min(1),
|
|
334
|
+
}),
|
|
335
|
+
}),
|
|
336
|
+
async handler(req, res) {
|
|
337
|
+
const { token } = req.input.query;
|
|
338
|
+
|
|
339
|
+
// Verifies token, consumes it, marks email verified, logs into session and/or generates JWTs
|
|
340
|
+
const result = await req.auth.loginWithMagicLink(token, {
|
|
341
|
+
remember: true,
|
|
342
|
+
jwt: false, // Set to true to receive accessToken and refreshToken
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// For browser requests, redirect to dashboard:
|
|
346
|
+
if (req.accepts('html')) {
|
|
347
|
+
return res.redirect('/dashboard');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return res.json({ ok: true, user: result.user });
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### 7.4 Using Built-in Route Middlewares
|
|
356
|
+
You can also mount Webspresso's pre-configured Express middlewares directly:
|
|
357
|
+
```javascript
|
|
358
|
+
const {
|
|
359
|
+
createMagicLinkRequestHandler,
|
|
360
|
+
createMagicLinkVerifyMiddleware,
|
|
361
|
+
} = require('webspresso/core/auth');
|
|
362
|
+
|
|
363
|
+
// Request link: POST /auth/magic-link
|
|
364
|
+
app.post('/auth/magic-link', createMagicLinkRequestHandler(authManager));
|
|
365
|
+
|
|
366
|
+
// Verify & Login: GET /auth/magic-link/verify
|
|
367
|
+
app.get('/auth/magic-link/verify', createMagicLinkVerifyMiddleware(authManager, {
|
|
368
|
+
redirectTo: '/dashboard',
|
|
369
|
+
failureRedirect: '/login?error=invalid_magic_link',
|
|
370
|
+
jwt: false,
|
|
371
|
+
}));
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### 7.5 AuthManager Magic Link Events
|
|
375
|
+
`AuthManager` extends `EventEmitter`, allowing you to hook into magic link lifecycle events:
|
|
376
|
+
```javascript
|
|
377
|
+
authManager.on('magicLink:requested', ({ userId, email, expiresAt }) => {
|
|
378
|
+
console.log(`Magic link requested for ${email}, expires at ${expiresAt}`);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
authManager.on('magicLink:verified', ({ userId }) => {
|
|
382
|
+
console.log(`User ${userId} signed in via magic link`);
|
|
383
|
+
});
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
## 8. Active Devices & Remote Session Revocation
|
|
389
|
+
|
|
390
|
+
Webspresso provides a zero-dependency session management and device tracking system (`webspresso/core/auth`) allowing users to view all active logins, detect browser/OS/hardware types, and remotely revoke unwanted sessions.
|
|
391
|
+
|
|
392
|
+
### 8.1 Database Migration
|
|
393
|
+
Create the `user_sessions` table using the built-in migration helper:
|
|
394
|
+
```javascript
|
|
395
|
+
const { createUserSessionsTable } = require('webspresso/core/auth');
|
|
396
|
+
|
|
397
|
+
exports.up = async function(knex) {
|
|
398
|
+
await createUserSessionsTable(knex);
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
exports.down = async function(knex) {
|
|
402
|
+
const { dropUserSessionsTable } = require('webspresso/core/auth');
|
|
403
|
+
await dropUserSessionsTable(knex);
|
|
404
|
+
};
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
Schema columns:
|
|
408
|
+
- `id`: Bigint primary key
|
|
409
|
+
- `session_id`: Unique Express session ID
|
|
410
|
+
- `user_id`: User ID reference
|
|
411
|
+
- `ip_address`: Proxy-safe client IP (`x-forwarded-for`, `x-real-ip`, or socket)
|
|
412
|
+
- `user_agent`: Raw User-Agent string
|
|
413
|
+
- `browser`: Detected browser and version (e.g. `Chrome 128`, `Safari 17`, `Firefox 125`, `Edge 122`)
|
|
414
|
+
- `os`: Detected operating system (e.g. `macOS 14`, `Windows 10/11`, `iOS 17.4`, `Android 14`, `Ubuntu Linux`)
|
|
415
|
+
- `device`: Device form factor (`desktop`, `mobile`, `tablet`, `bot`)
|
|
416
|
+
- `last_active_at`: Last seen timestamp (throttled to 60s by default to protect DB performance)
|
|
417
|
+
- `created_at`: Initial login timestamp
|
|
418
|
+
|
|
419
|
+
### 8.2 Enabling Session Tracking
|
|
420
|
+
Enable session tracking in `createAuth` or `quickAuth`:
|
|
421
|
+
```javascript
|
|
422
|
+
const { quickAuth } = require('webspresso/core/auth');
|
|
423
|
+
|
|
424
|
+
const auth = quickAuth({
|
|
425
|
+
db,
|
|
426
|
+
sessionTracking: true, // Automatically enables Knex session tracker and active session verification
|
|
427
|
+
});
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
Or manually with custom options:
|
|
431
|
+
```javascript
|
|
432
|
+
const { createAuth, createKnexSessionTracker } = require('webspresso/core/auth');
|
|
433
|
+
|
|
434
|
+
const auth = createAuth({
|
|
435
|
+
db,
|
|
436
|
+
sessionTracking: true,
|
|
437
|
+
sessionTracker: createKnexSessionTracker(db),
|
|
438
|
+
sessionTouchThrottleMs: 60000, // Touch session at most once per minute
|
|
439
|
+
// ...other config
|
|
440
|
+
});
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### 8.3 File-Based API Routes (`pages/api/*`)
|
|
444
|
+
|
|
445
|
+
#### List Active Devices (`pages/api/auth/sessions.get.js`)
|
|
446
|
+
```javascript
|
|
447
|
+
const { defineApi } = require('webspresso');
|
|
448
|
+
|
|
449
|
+
module.exports = defineApi({
|
|
450
|
+
middleware: ['auth'],
|
|
451
|
+
async handler(req, res) {
|
|
452
|
+
// Returns array of active sessions with is_current flag for current device
|
|
453
|
+
const sessions = await req.auth.sessions();
|
|
454
|
+
return res.json({ ok: true, sessions });
|
|
455
|
+
},
|
|
456
|
+
});
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
#### Revoke Session / Remote Logout (`pages/api/auth/sessions/revoke.post.js`)
|
|
460
|
+
```javascript
|
|
461
|
+
const { defineApi } = require('webspresso');
|
|
462
|
+
|
|
463
|
+
module.exports = defineApi({
|
|
464
|
+
middleware: ['auth'],
|
|
465
|
+
schema: ({ z }) => ({
|
|
466
|
+
body: z.object({
|
|
467
|
+
sessionId: z.string().optional(),
|
|
468
|
+
allOthers: z.boolean().optional(),
|
|
469
|
+
}),
|
|
470
|
+
}),
|
|
471
|
+
async handler(req, res) {
|
|
472
|
+
const { sessionId, allOthers } = req.input.body;
|
|
473
|
+
|
|
474
|
+
if (allOthers) {
|
|
475
|
+
// Revokes all sessions except current device
|
|
476
|
+
const count = await req.auth.revokeOtherSessions();
|
|
477
|
+
return res.json({ ok: true, revokedCount: count, message: 'All other sessions revoked' });
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (sessionId) {
|
|
481
|
+
// Revokes specified session (logs out current user if it is the current session)
|
|
482
|
+
await req.auth.revokeSession(sessionId);
|
|
483
|
+
return res.json({ ok: true, message: 'Session revoked' });
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return res.status(400).json({ error: 'sessionId or allOthers is required' });
|
|
487
|
+
},
|
|
488
|
+
});
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
### 8.4 Automatic Invalidation in Middleware
|
|
492
|
+
When a session is revoked remotely, Webspresso's `authenticate` middleware automatically detects it on the next incoming request from that device:
|
|
493
|
+
- The revoked session is destroyed in `req.sessionStore`.
|
|
494
|
+
- `req.user` is set to `null`.
|
|
495
|
+
- Any subsequent `requireAuth` or `middleware: ['auth']` guard immediately redirects or rejects the request with `401 Unauthorized`.
|
|
496
|
+
|
|
497
|
+
### 8.5 Session Lifecycle Events
|
|
498
|
+
`AuthManager` emits EventEmitter events for monitoring and security audits:
|
|
499
|
+
```javascript
|
|
500
|
+
auth.on('session:created', ({ sessionId, userId, ip, browser, os, device }) => {
|
|
501
|
+
console.log(`New session created for user ${userId} from ${device} (${browser} on ${os}) at ${ip}`);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
auth.on('session:revoked', ({ sessionId, userId, revokedBy }) => {
|
|
505
|
+
console.log(`Session ${sessionId} was revoked`);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
auth.on('sessions:revoked_others', ({ userId, currentSessionId, count }) => {
|
|
509
|
+
console.log(`User ${userId} revoked ${count} other sessions`);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
auth.on('sessions:revoked_all', ({ userId, count }) => {
|
|
513
|
+
console.log(`All ${count} sessions revoked for user ${userId}`);
|
|
514
|
+
});
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
|
package/.agents/CLI.md
CHANGED
|
@@ -302,8 +302,55 @@ npx webspresso typegen --out types/webspresso.d.ts --no-env-file
|
|
|
302
302
|
- **Typed Services (`req.service('name', input)`)**: Auto-completes registered service names and enforces validated input payloads.
|
|
303
303
|
- **Typed Route Table (`AppRoute`, `AppRoutePath`)**: Union types for all discovered SSR and API routes.
|
|
304
304
|
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## 16. Containerization & Docker Setup (`add:docker` / `docker`)
|
|
308
|
+
|
|
309
|
+
Generate production-ready, security-hardened, multi-stage `Dockerfile`, `.dockerignore`, and `docker-compose.yml`:
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
# Generate Dockerfile, .dockerignore, and docker-compose.yml
|
|
313
|
+
npx webspresso add:docker
|
|
314
|
+
|
|
315
|
+
# Customize Node.js version, database service, or container port
|
|
316
|
+
npx webspresso add:docker --node 22-alpine --db postgres --redis --port 3000
|
|
305
317
|
|
|
318
|
+
# Overwrite existing files
|
|
319
|
+
npx webspresso add:docker --force
|
|
320
|
+
```
|
|
306
321
|
|
|
322
|
+
### Generated Artifacts:
|
|
323
|
+
- **Multi-Stage `Dockerfile`**:
|
|
324
|
+
- `deps` stage: caches production dependencies with native build tools (`python3`, `make`, `g++`).
|
|
325
|
+
- `builder` stage: runs asset build scripts (`npm run build`).
|
|
326
|
+
- `runner` stage: runs as unprivileged user `webspresso` (UID 1001), health check with `/health` and `/healthz`, and `SIGTERM` signal handling.
|
|
327
|
+
- **`.dockerignore`**: Excludes `node_modules`, `.env`, local `.sqlite` databases, test files, and git metadata while preserving upload directories.
|
|
328
|
+
- **`docker-compose.yml`**: Configures app container with persistent upload volumes, database services (PostgreSQL, MySQL, or SQLite), and optional Redis.
|
|
329
|
+
- **`package.json` Scripts**: Adds `docker:build`, `docker:run`, `docker:up`, and `docker:down`.
|
|
307
330
|
|
|
331
|
+
---
|
|
308
332
|
|
|
333
|
+
## 17. Production Deployment Preflight Check (`deploy:check` / `check:deploy`)
|
|
334
|
+
|
|
335
|
+
Run comprehensive pre-flight verification before deploying to Fly.io, Railway, Render, VPS, or Kubernetes:
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
# Run production preflight verification
|
|
339
|
+
npx webspresso deploy:check
|
|
340
|
+
|
|
341
|
+
# Run against a specific environment (.env.staging or .env.production)
|
|
342
|
+
npx webspresso deploy:check --env staging
|
|
343
|
+
|
|
344
|
+
# Strict mode: fail with exit code 1 on warnings as well as errors (ideal for CI/CD)
|
|
345
|
+
npx webspresso deploy:check --strict
|
|
346
|
+
|
|
347
|
+
# Machine-readable JSON output for automated deployment pipelines
|
|
348
|
+
npx webspresso deploy:check --json
|
|
349
|
+
```
|
|
309
350
|
|
|
351
|
+
### Audited Verification Areas:
|
|
352
|
+
1. **Secrets & Cryptography**: Ensures `SESSION_SECRET` or `APP_KEY` is set, >= 32 characters, and not using weak placeholder defaults.
|
|
353
|
+
2. **Git & Leak Prevention**: Verifies `.env` and local database files are excluded in `.gitignore`.
|
|
354
|
+
3. **Container Hardening**: Ensures `Dockerfile` runs as a non-root `USER`, configures a `HEALTHCHECK`, and uses multi-stage caching.
|
|
355
|
+
4. **Database & Migrations**: Audits for pending unapplied migrations and warns against ephemeral SQLite storage.
|
|
356
|
+
5. **Server Optimizations**: Validates HTTP compression (`compression: true`), graceful shutdown, and health check endpoints.
|
package/.agents/ERRORS.md
CHANGED
|
@@ -199,3 +199,62 @@ In server logs, 500 errors are automatically highlighted with visual banners pin
|
|
|
199
199
|
Phase: handler
|
|
200
200
|
```
|
|
201
201
|
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## 6. Directory-Level Error Boundaries (`_error.njk` & `_error.js`)
|
|
205
|
+
|
|
206
|
+
In addition to API JSON errors and the top-level 500 error page, Webspresso provides **Directory-Level Error Boundaries** for SSR pages (similar to Next.js `error.js` or Nuxt `error.vue`):
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
pages/
|
|
210
|
+
├── _layout.njk # Root site layout
|
|
211
|
+
├── _error.njk # Global site error fallback
|
|
212
|
+
└── dashboard/
|
|
213
|
+
├── _layout.njk # Dashboard layout (sidebar, header)
|
|
214
|
+
├── _error.njk # Dashboard-scoped error boundary
|
|
215
|
+
├── _error.js # Optional companion loader
|
|
216
|
+
├── billing/
|
|
217
|
+
│ └── invoices.njk # If load() fails here...
|
|
218
|
+
└── settings.njk
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### 6.1 Isolated Error Containment
|
|
222
|
+
When an unhandled error is thrown in a page route's `load()`, template rendering, or lifecycle hooks:
|
|
223
|
+
1. Webspresso searches up the directory hierarchy (innermost leaf to root `pages/`) for the nearest `_error.njk`.
|
|
224
|
+
2. The error boundary is rendered **inside** the ancestor cascading layouts (`_layout.njk`), ensuring that headers, sidebars, and navigation remain visible while displaying the isolated error state.
|
|
225
|
+
3. The HTTP response status is preserved from `error.status || error.statusCode || 500` (e.g. throwing `NotFoundError` sets HTTP 404).
|
|
226
|
+
|
|
227
|
+
### 6.2 Companion Loader (`_error.js`)
|
|
228
|
+
An optional companion `_error.js` can prefetch support data, report errors to monitoring services, or enrich the context:
|
|
229
|
+
|
|
230
|
+
```javascript
|
|
231
|
+
// pages/dashboard/_error.js
|
|
232
|
+
module.exports = {
|
|
233
|
+
load: async ({ error, req, res, ctx, db }) => {
|
|
234
|
+
return {
|
|
235
|
+
supportEmail: 'support@example.com',
|
|
236
|
+
incidentId: req.id,
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### 6.3 Template Context Variables
|
|
243
|
+
Inside `_error.njk`, the following variables are injected:
|
|
244
|
+
- `error`: `{ message, status, statusCode, code, name, details, fields, stack }` (stack is only exposed when `isDev === true`).
|
|
245
|
+
- `route`: `{ routePath, file }`.
|
|
246
|
+
- `req`: `{ path, query, params, method, url }`.
|
|
247
|
+
- `fsy`: Template helper suite.
|
|
248
|
+
- Plus any data returned by companion `_error.js` loader.
|
|
249
|
+
|
|
250
|
+
### 6.4 Layout Opt-Out
|
|
251
|
+
To render a full-page custom error view without the parent layout, set `layout: false` in `_error.js` or frontmatter:
|
|
252
|
+
```yaml
|
|
253
|
+
---
|
|
254
|
+
layout: false
|
|
255
|
+
---
|
|
256
|
+
<div class="standalone-error-screen">
|
|
257
|
+
<h1>{{ error.status }} - {{ error.message }}</h1>
|
|
258
|
+
</div>
|
|
259
|
+
```
|
|
260
|
+
|