webspresso 0.0.99 → 0.0.101
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 +47 -4
- package/.agents/AGENTS.md +4 -2
- package/.agents/AUTH.md +309 -0
- package/.agents/CLI.md +128 -0
- package/.agents/EMAIL.md +84 -2
- package/.agents/ERRORS.md +59 -0
- package/.agents/EVENTS.md +179 -0
- package/.agents/ORM.md +149 -0
- package/.agents/PLUGINS.md +44 -7
- package/.agents/QUEUE.md +18 -10
- package/.agents/ROUTING.md +97 -1
- 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/db-makemigrations.js +83 -0
- package/bin/commands/deploy-check.js +339 -0
- package/bin/commands/list.js +96 -0
- package/bin/commands/make-command.js +93 -0
- package/bin/commands/make-event.js +80 -0
- package/bin/commands/make-factory.js +72 -0
- package/bin/commands/make-mail.js +93 -0
- package/bin/commands/make-policy.js +106 -0
- package/bin/commands/run.js +195 -0
- package/bin/webspresso.js +21 -0
- package/core/auth/bouncer.js +196 -0
- package/core/auth/index.js +73 -1
- package/core/auth/magic-link.js +170 -0
- package/core/auth/manager.js +343 -3
- package/core/auth/middleware.js +45 -6
- 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 +4 -1
- package/core/auth/user-agent.js +161 -0
- package/core/commands/define-command.js +48 -0
- package/core/commands/index.js +18 -0
- package/core/commands/registry.js +156 -0
- package/core/commands/runner.js +76 -0
- package/core/compression/compressible.js +1 -1
- package/core/compression/middleware.js +35 -251
- package/core/content/service.js +8 -2
- package/core/events/discovery.js +142 -0
- package/core/events/dispatcher.js +552 -0
- package/core/events/index.js +23 -0
- package/core/events/signal.js +92 -0
- package/core/forms/flash.js +234 -0
- package/core/forms/form-builder.js +214 -0
- package/core/forms/index.js +16 -0
- package/core/mail/index.js +23 -0
- package/core/mail/mailable.js +250 -0
- package/core/mail/mailer.js +184 -0
- package/core/mail/transports.js +238 -0
- package/core/orm/factory.js +250 -0
- package/core/orm/index.js +8 -1
- package/core/orm/migrations/diff.js +185 -0
- package/core/orm/migrations/index.js +22 -4
- package/core/orm/model.js +55 -1
- package/core/orm/repository.js +283 -13
- package/core/orm/scopes.js +3 -2
- package/core/orm/types.js +6 -1
- package/core/queue/cron.js +252 -39
- package/core/queue/manager.js +20 -2
- package/core/queue/scheduler.js +115 -59
- package/core/ssr/stream.js +62 -2
- package/index.d.ts +540 -14
- package/index.js +44 -0
- package/package.json +11 -5
- package/plugins/admin-panel/api.js +509 -3
- package/plugins/admin-panel/index.js +17 -0
- package/plugins/admin-panel/modules/admin-users.js +1 -1
- package/plugins/audit-log/admin-component.js +54 -54
- package/plugins/audit-log/index.js +2 -2
- package/plugins/content/client/inline-edit.js +2 -46
- package/plugins/content/models/content-entry.js +4 -3
- package/plugins/content/models/content-type.js +4 -3
- package/plugins/cors.js +15 -4
- package/plugins/email/auth-bridge.js +66 -0
- package/plugins/email/bundled-templates.js +15 -0
- package/plugins/email/index.js +48 -0
- package/plugins/email/preview-handler.js +555 -0
- package/plugins/email/service.js +23 -12
- package/plugins/email/templates/auth-magic-link.mjml +13 -0
- package/plugins/email/transport.js +30 -11
- package/plugins/file-manager/admin-component.js +49 -49
- package/plugins/file-manager/index.js +4 -4
- package/plugins/index.js +2 -0
- package/plugins/redirect/index.js +1 -1
- 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 -1
- package/src/app-context.js +48 -6
- package/src/bundle/index.js +2 -1
- package/src/dev-reload/index.js +9 -1
- package/src/discovery/scan-routes.js +10 -1
- package/src/file-router.js +173 -4
- package/src/helpers.js +150 -1
- package/src/njk-frontmatter.js +3 -0
- package/src/pages/page-loader.js +1 -1
- package/src/routing/cascading.js +120 -4
- package/src/server.js +180 -26
package/.agents/ADMIN.md
CHANGED
|
@@ -46,6 +46,31 @@ export default defineModel({
|
|
|
46
46
|
sortableColumns: ['name', 'price'], // Optional whitelist of sortable columns
|
|
47
47
|
sortable: true, // Set false to disable table sorting completely
|
|
48
48
|
readOnly: false, // Disable create/edit/delete actions
|
|
49
|
+
|
|
50
|
+
// Django Admin-inspired Features:
|
|
51
|
+
inlines: [ // Parent-child relational editing (Tabular/Stacked)
|
|
52
|
+
{
|
|
53
|
+
model: 'ProductVariant',
|
|
54
|
+
foreignKey: 'product_id',
|
|
55
|
+
fields: ['sku', 'price', 'inventory'],
|
|
56
|
+
title: 'Variants',
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
listEditable: ['price', 'status'], // Direct table row quick-edit without opening edit form
|
|
60
|
+
dateHierarchy: 'created_at', // Period drilldown navigation (Years -> Months -> Days)
|
|
61
|
+
listFilter: ['status'], // Faceted filtering with record counts per value
|
|
62
|
+
actions: [ // Declarative model-level bulk operations
|
|
63
|
+
{
|
|
64
|
+
id: 'mark-active',
|
|
65
|
+
label: 'Mark as Active',
|
|
66
|
+
handler: async ({ ids, repo, db, req, user }) => {
|
|
67
|
+
for (const id of ids) {
|
|
68
|
+
await repo.update(id, { status: 'active' });
|
|
69
|
+
}
|
|
70
|
+
return { updated: ids.length };
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
],
|
|
49
74
|
},
|
|
50
75
|
});
|
|
51
76
|
```
|
|
@@ -68,13 +93,31 @@ Column sortability can be configured per-column:
|
|
|
68
93
|
The Admin SPA communicates with the backend via JSON REST endpoints:
|
|
69
94
|
|
|
70
95
|
- `GET /_admin/api/models` — List all models exposed to admin with schemas & metadata.
|
|
71
|
-
- `GET /_admin/api/models/:modelName/records` — List paginated records (query params: `page`, `limit`, `search`, `filter`, `sort`).
|
|
72
|
-
- `GET /_admin/api/models/:modelName/records/:id` — Retrieve single record details.
|
|
73
|
-
- `POST /_admin/api/models/:modelName/records` — Create new record.
|
|
74
|
-
- `PUT /_admin/api/models/:modelName/records/:id` — Update existing record.
|
|
96
|
+
- `GET /_admin/api/models/:modelName/records` — List paginated records (query params: `page`, `limit`, `search`, `filter`, `sort`, `year`, `month`, `day`).
|
|
97
|
+
- `GET /_admin/api/models/:modelName/records/:id` — Retrieve single record details (auto-loads `_inlines` if defined).
|
|
98
|
+
- `POST /_admin/api/models/:modelName/records` — Create new record (syncs `_inlines` automatically).
|
|
99
|
+
- `PUT /_admin/api/models/:modelName/records/:id` — Update existing record (syncs `_inlines` updates, additions, and deletions with `_delete: true`).
|
|
75
100
|
- `DELETE /_admin/api/models/:modelName/records/:id` — Delete record (soft or hard delete).
|
|
76
101
|
- `POST /_admin/api/models/:modelName/records/:id/restore` — Restore soft-deleted record.
|
|
77
102
|
|
|
103
|
+
### Inline Model Relational Endpoints (Django Inlines)
|
|
104
|
+
- `GET /_admin/api/models/:model/records/:id/inlines/:inlineModel` — List child items belonging to parent record.
|
|
105
|
+
- `POST /_admin/api/models/:model/records/:id/inlines/:inlineModel` — Create a child item attached to parent.
|
|
106
|
+
- `PUT /_admin/api/models/:model/records/:id/inlines/:inlineModel/:inlineId` — Update specific child item.
|
|
107
|
+
- `DELETE /_admin/api/models/:model/records/:id/inlines/:inlineModel/:inlineId` — Delete child item.
|
|
108
|
+
|
|
109
|
+
### Fast Inline & Batch Editing (Django `list_editable`)
|
|
110
|
+
- `PATCH /_admin/api/models/:model/records/:id/quick-edit` — Quickly update allowed `listEditable` columns for a single row without full form submission.
|
|
111
|
+
- `PATCH /_admin/api/models/:model/records/batch-edit` — Save multiple table row edits simultaneously (`{ updates: [{ id, changes }] }`).
|
|
112
|
+
|
|
113
|
+
### Date Hierarchy & Faceted Filters (Django `date_hierarchy` & `list_filter`)
|
|
114
|
+
- `GET /_admin/api/models/:model/date-hierarchy` — Drilldown periods with counts (`?year=...&month=...`).
|
|
115
|
+
- `GET /_admin/api/models/:model/filter-facets` — Retrieve distinct values with record counts for `listFilter` columns.
|
|
116
|
+
|
|
117
|
+
### Model Actions (Django Actions)
|
|
118
|
+
- `POST /_admin/api/models/:model/actions/:actionId` — Execute declarative action on selected record IDs (`{ ids: [...] }`).
|
|
119
|
+
|
|
120
|
+
|
|
78
121
|
---
|
|
79
122
|
|
|
80
123
|
## 4. Custom Pages (`registerModule` / `registerPageDir`)
|
package/.agents/AGENTS.md
CHANGED
|
@@ -17,6 +17,7 @@ When tasked with a feature or bugfix, **immediately consult the corresponding to
|
|
|
17
17
|
| **Admin Panel (Page, Widget, Action, UI)** | [`.agents/ADMIN.md`](ADMIN.md) | `plugins/admin-panel/` |
|
|
18
18
|
| **Authentication (Session / JWT / Guard)** | [`.agents/AUTH.md`](AUTH.md) | `core/auth/` |
|
|
19
19
|
| **Background Jobs & Worker Queue** | [`.agents/QUEUE.md`](QUEUE.md) | `jobs/` |
|
|
20
|
+
| **Decoupled Events & Signals** | [`.agents/EVENTS.md`](EVENTS.md) | `core/events/`, `listeners/` |
|
|
20
21
|
| **API Signatures & Types Verification** | [`index.d.ts`](../index.d.ts) | **Canonical Ground Truth** |
|
|
21
22
|
|
|
22
23
|
---
|
|
@@ -44,16 +45,17 @@ This `.agents/` customization root is organized into modular topic guides:
|
|
|
44
45
|
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
46
|
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
47
|
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
|
|
48
|
+
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
49
|
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
50
|
11. **[Email Plugin Guide](.agents/EMAIL.md)** — MJML template compilation, Nodemailer transport, DB delivery logs, Auth Email bridge, and Admin UI.
|
|
50
51
|
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](
|
|
52
|
+
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
53
|
14. **[Development & Testing Workflow](.agents/WORKFLOW.md)** — CLI commands, Vitest unit/integration testing, Playwright E2E tests, TypeScript type checking, and zero regression policy.
|
|
53
54
|
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
55
|
16. **[Background Job Queue](.agents/QUEUE.md)** — In-Memory, Knex Database, and Distributed Redis queue adapters, job auto-discovery (`jobs/`), retries, and graceful draining.
|
|
55
56
|
17. **[SSR Streaming & Chunked Transfer](.agents/STREAMING.md)** — Native HTTP chunked streaming (`res.renderStream()`), deferred data loader slots (`defer: { key: promise }`), and TTFB optimization.
|
|
56
57
|
18. **Polar billing production checklist** — [docs/guides/polar-production-checklist.md](../docs/guides/polar-production-checklist.md) (env, webhooks, rate limits, external tier hooks, CSP). Plugin reference: [PLUGINS.md § polarPlugin](PLUGINS.md).
|
|
58
|
+
19. **[Decoupled Events & Signals Architecture](.agents/EVENTS.md)** — Zero-dependency enterprise event dispatcher (`core/events`), sync/async/parallel/serial/waterfall dispatch, wildcard listeners (`order.*`), Django-style `Signal` and sender filtering, queue bridging, and listener auto-discovery.
|
|
57
59
|
|
|
58
60
|
---
|
|
59
61
|
|
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
|
|
|
@@ -269,4 +273,309 @@ module.exports = {
|
|
|
269
273
|
};
|
|
270
274
|
```
|
|
271
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
|
+
|
|
519
|
+
## 9. Authorization & Policies (AdonisJS Bouncer & Django Permissions-inspired)
|
|
520
|
+
|
|
521
|
+
Webspresso provides an authorization layer combining AdonisJS Bouncer policies/gates with Django-style permission matrices and superuser bypass:
|
|
522
|
+
|
|
523
|
+
### 9.1 Defining Policies & Gates
|
|
524
|
+
Define domain policies and simple gates using `definePolicy` and `defineGate`:
|
|
525
|
+
|
|
526
|
+
```javascript
|
|
527
|
+
const { definePolicy, defineGate } = require('webspresso');
|
|
528
|
+
|
|
529
|
+
// 1. Resource Policy
|
|
530
|
+
definePolicy('Post', {
|
|
531
|
+
view: (user, post) => true,
|
|
532
|
+
update: (user, post) => user && (user.id === post.user_id || user.role === 'admin'),
|
|
533
|
+
delete: (user, post) => user && user.role === 'admin',
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// 2. Named Gate
|
|
537
|
+
defineGate('admin-only', (user) => Boolean(user && user.role === 'admin'));
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### 9.2 Request Bouncer (`req.bouncer`)
|
|
541
|
+
Incoming requests automatically receive `req.bouncer` bound to `req.user`:
|
|
542
|
+
|
|
543
|
+
```javascript
|
|
544
|
+
// Inside API Route or Loader:
|
|
545
|
+
if (req.bouncer.allows('Post.update', post)) {
|
|
546
|
+
// Authorized
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (req.bouncer.denies('admin-only')) {
|
|
550
|
+
// Denied
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Throws ForbiddenError (403) or UnauthorizedError (401) on failure:
|
|
554
|
+
req.bouncer.authorize('Post.update', post);
|
|
555
|
+
|
|
556
|
+
// Inline rule check:
|
|
557
|
+
req.bouncer.authorize((user) => user.isVip);
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
### 9.3 Django Permissions Matrix & Superuser Bypass
|
|
561
|
+
Users with permissions arrays or superuser flags are evaluated automatically:
|
|
562
|
+
- `user.permissions = ['post.create', 'post.update']`
|
|
563
|
+
- Wildcards: `user.permissions = ['post.*']` or `['*']`
|
|
564
|
+
- Django Superuser: `user.isSuperuser = true` or `user.is_superuser = true` automatically bypasses all permission checks.
|
|
565
|
+
- Custom methods: `user.hasPerm(perm, resource)` or `user.hasPermission(...)`.
|
|
566
|
+
|
|
567
|
+
### 9.4 Nunjucks Template Helpers (`fsy.can`, `fsy.cannot`)
|
|
568
|
+
Check permissions directly in `.njk` templates:
|
|
569
|
+
|
|
570
|
+
```jinja
|
|
571
|
+
{% if fsy.can('Post.update', post) %}
|
|
572
|
+
<a href="/posts/{{ post.id }}/edit" class="btn">Edit Post</a>
|
|
573
|
+
{% endif %}
|
|
574
|
+
|
|
575
|
+
{% if fsy.cannot('admin-only') %}
|
|
576
|
+
<p class="text-muted">Admin access required</p>
|
|
577
|
+
{% endif %}
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
|
|
272
581
|
|
package/.agents/CLI.md
CHANGED
|
@@ -302,8 +302,136 @@ 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
|
|
317
|
+
|
|
318
|
+
# Overwrite existing files
|
|
319
|
+
npx webspresso add:docker --force
|
|
320
|
+
```
|
|
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`.
|
|
330
|
+
|
|
331
|
+
---
|
|
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
|
+
```
|
|
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.
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## 18. Custom Management Commands (`run`, `command:run`, `defineCommand`) [Django manage.py & Adonis Ace]
|
|
361
|
+
|
|
362
|
+
Create and execute isolated management scripts, background cron tasks, data migrations, or maintenance routines with full framework dependency injection (`db`, `queue`, `services`, `mailer`, `events`, `logger`, `args`, `flags`):
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
# List all custom management commands in commands/
|
|
366
|
+
npx webspresso run
|
|
367
|
+
|
|
368
|
+
# Execute a custom command with positional arguments and options
|
|
369
|
+
npx webspresso run sync-stripe --dry-run --limit 100
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Defining a Custom Command (`commands/<name>.js`):
|
|
305
373
|
|
|
374
|
+
```javascript
|
|
375
|
+
const { defineCommand } = require('webspresso');
|
|
306
376
|
|
|
377
|
+
module.exports = defineCommand({
|
|
378
|
+
name: 'sync-stripe',
|
|
379
|
+
description: 'Synchronize customer subscriptions from Stripe',
|
|
380
|
+
options: [
|
|
381
|
+
{ flags: '-d, --dry-run', description: 'Simulate without committing changes', defaultValue: false },
|
|
382
|
+
{ flags: '-l, --limit <count>', description: 'Maximum customers to sync', defaultValue: 100 },
|
|
383
|
+
],
|
|
384
|
+
handle: async ({ db, queue, services, mailer, events, logger, args, flags }) => {
|
|
385
|
+
logger.log('Starting sync, dryRun:', flags.dryRun);
|
|
307
386
|
|
|
387
|
+
const userRepo = db.getRepository('User');
|
|
388
|
+
const users = await userRepo.find({ active: true });
|
|
389
|
+
|
|
390
|
+
// Events, mailer, and background jobs are fully accessible:
|
|
391
|
+
events.emit('stripe:sync-started', { count: users.length });
|
|
392
|
+
|
|
393
|
+
return { synced: users.length };
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
```
|
|
308
397
|
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## 19. Command Palette Index (`list`, `commands`)
|
|
401
|
+
|
|
402
|
+
Print an organized catalog of all built-in commands (scaffolding, database, server, developer tools) alongside all auto-discovered custom commands in your project:
|
|
403
|
+
|
|
404
|
+
```bash
|
|
405
|
+
npx webspresso list
|
|
406
|
+
# or
|
|
407
|
+
npx webspresso commands
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
## 20. Advanced Scaffolding Generators (`make:*`) [Adonis Ace style]
|
|
413
|
+
|
|
414
|
+
Rapidly generate boilerplate code following Webspresso architectural conventions:
|
|
415
|
+
|
|
416
|
+
```bash
|
|
417
|
+
# 1. Custom Management Command
|
|
418
|
+
npx webspresso make:command users:cleanup --desc "Purge unverified user accounts"
|
|
419
|
+
# Outputs: commands/users/cleanup.js
|
|
420
|
+
|
|
421
|
+
# 2. Authorization Policy (Bouncer)
|
|
422
|
+
npx webspresso make:policy Article
|
|
423
|
+
# Outputs: policies/article.policy.js
|
|
424
|
+
|
|
425
|
+
# 3. Model Factory (Testing & Seeding)
|
|
426
|
+
npx webspresso make:factory Product
|
|
427
|
+
# Outputs: factories/product.factory.js
|
|
428
|
+
|
|
429
|
+
# 4. Decoupled Event Listener
|
|
430
|
+
npx webspresso make:event order:completed
|
|
431
|
+
# Outputs: listeners/order-completed.js
|
|
432
|
+
|
|
433
|
+
# 5. Declarative Mailable Class
|
|
434
|
+
npx webspresso make:mail Welcome
|
|
435
|
+
# Outputs: emails/WelcomeEmail.js
|
|
436
|
+
```
|
|
309
437
|
|
package/.agents/EMAIL.md
CHANGED
|
@@ -102,11 +102,92 @@ emailApi.registerTemplate('notification', '<mjml><mj-body>...</mj-body></mjml>')
|
|
|
102
102
|
const isReady = await emailApi.verifyConnection();
|
|
103
103
|
const logs = await emailApi.queryLogs({ status: 'sent', limit: 20 });
|
|
104
104
|
await emailApi.purgeLogs({ olderThan: '30d' });
|
|
105
|
+
|
|
106
|
+
// 6. In-memory outbox inspection & clearing (memory backend / tests)
|
|
107
|
+
const outbox = emailApi.getOutbox();
|
|
108
|
+
const lastSent = emailApi.lastMessage();
|
|
109
|
+
emailApi.clearOutbox();
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## 5. Declarative `Mailable` Classes (AdonisJS-style)
|
|
115
|
+
|
|
116
|
+
Define structured, reusable, object-oriented emails by extending `Mailable`:
|
|
117
|
+
|
|
118
|
+
```javascript
|
|
119
|
+
import { Mailable } from 'webspresso';
|
|
120
|
+
|
|
121
|
+
export class WelcomeEmail extends Mailable {
|
|
122
|
+
constructor(user) {
|
|
123
|
+
super();
|
|
124
|
+
this.user = user;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
build() {
|
|
128
|
+
this.to(this.user.email, this.user.name)
|
|
129
|
+
.from('team@myapp.com', 'Acme Team')
|
|
130
|
+
.subject(`Welcome to Acme, ${this.user.name}!`)
|
|
131
|
+
.view('welcome', { user: this.user }) // references registered template
|
|
132
|
+
.attach({
|
|
133
|
+
filename: 'welcome-guide.pdf',
|
|
134
|
+
content: Buffer.from('...'),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// In route handlers or services:
|
|
140
|
+
await req.mailer.send(new WelcomeEmail(user));
|
|
141
|
+
// Or via email API:
|
|
142
|
+
await emailApi.send(new WelcomeEmail(user));
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Fluent Mailable Methods
|
|
146
|
+
|
|
147
|
+
- `to(address, name?)`, `cc(address, name?)`, `bcc(address, name?)`, `replyTo(address, name?)`, `from(address, name?)`
|
|
148
|
+
- `subject(title)`
|
|
149
|
+
- `view(templateName, data?)` or `template(templateName)`
|
|
150
|
+
- `html(htmlContent)`, `text(textContent)`
|
|
151
|
+
- `attach({ filename, content, path, contentType })`
|
|
152
|
+
- `header(name, value)`
|
|
153
|
+
- `prepare()` and `build()` lifecycle hooks
|
|
154
|
+
- `toMessage()` serializes the definition into a message payload
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## 6. Pluggable Transports (Django-style Backends)
|
|
159
|
+
|
|
160
|
+
Configure backends via `backend`: `'smtp' | 'memory' | 'console' | 'file'`:
|
|
161
|
+
|
|
162
|
+
```javascript
|
|
163
|
+
const email = emailPlugin({
|
|
164
|
+
backend: process.env.NODE_ENV === 'production' ? 'smtp' : 'memory',
|
|
165
|
+
fileDir: './storage/emails', // when backend: 'file'
|
|
166
|
+
devInbox: true, // enables developer inbox web UI
|
|
167
|
+
});
|
|
105
168
|
```
|
|
106
169
|
|
|
170
|
+
- **`MemoryTransport`**: Stores messages in an in-memory inspectable outbox (`mailer.getOutbox()`). Automatically chosen in test environments.
|
|
171
|
+
- **`ConsoleTransport`**: Pretty-prints sent emails to stdout/logger for zero-setup terminal debugging.
|
|
172
|
+
- **`FileTransport`**: Writes JSON/EML email files to a local directory for disk-based inspection.
|
|
173
|
+
- **`NodemailerTransport`**: Delivers real emails via SMTP or custom transport in production.
|
|
174
|
+
|
|
107
175
|
---
|
|
108
176
|
|
|
109
|
-
##
|
|
177
|
+
## 7. Developer Mail Preview & Dev Inbox (`/_webspresso/mail-preview`)
|
|
178
|
+
|
|
179
|
+
When `devInbox: true` or running in `development` mode, Webspresso automatically mounts a local inbox viewer:
|
|
180
|
+
|
|
181
|
+
- **Web UI Route**: `GET /_webspresso/mail-preview` (responsive inbox viewer with desktop, tablet, and mobile viewport switcher)
|
|
182
|
+
- **JSON List Route**: `GET /_webspresso/mail-preview/api/messages`
|
|
183
|
+
- **Single Message Route**: `GET /_webspresso/mail-preview/api/messages/:id`
|
|
184
|
+
- **Iframe HTML View**: `GET /_webspresso/mail-preview/api/messages/:id/html`
|
|
185
|
+
- **Clear Inbox Route**: `POST /_webspresso/mail-preview/api/clear`
|
|
186
|
+
- **Template Preview**: `POST /_webspresso/mail-preview/api/preview`
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## 8. Auth Email Bridge (`authEmails`)
|
|
110
191
|
|
|
111
192
|
Automatically wires password reset and email verification flows with `AuthManager`:
|
|
112
193
|
|
|
@@ -131,7 +212,7 @@ const email = emailPlugin({
|
|
|
131
212
|
|
|
132
213
|
---
|
|
133
214
|
|
|
134
|
-
##
|
|
215
|
+
## 9. Admin Panel UI & Database Logs
|
|
135
216
|
|
|
136
217
|
When `includeAdminPage: true` and `adminPanelPlugin` are present, an interactive Email Management module is mounted at `/_admin/email`:
|
|
137
218
|
|
|
@@ -139,3 +220,4 @@ When `includeAdminPage: true` and `adminPanelPlugin` are present, an interactive
|
|
|
139
220
|
- **Test Sender**: Send test emails directly from the Admin Panel to verify SMTP configuration.
|
|
140
221
|
- **Audit Logs Inspector**: Filter sent email logs by status (`sent`, `failed`), view error tracebacks, and purge historic logs.
|
|
141
222
|
- **Database Schema**: Log entries are stored in `email_logs` table (`id`, `to_email`, `subject`, `template_name`, `status`, `error`, `created_at`).
|
|
223
|
+
|