webspresso 0.0.72 → 0.0.74
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 +4 -2
- package/bin/commands/upgrade.js +146 -0
- package/bin/webspresso.js +2 -0
- package/package.json +1 -1
- package/plugins/admin-panel/app.js +109 -0
- package/plugins/admin-panel/components.js +111 -111
- package/plugins/admin-panel/modules/dashboard.js +16 -13
- package/plugins/admin-panel/modules/user-management.js +32 -11
- package/plugins/data-exchange/export-xlsx.js +3 -0
- package/plugins/data-exchange/record-selection.js +21 -5
- package/plugins/site-analytics/admin-component.js +88 -78
- package/src/file-router.js +80 -4
- package/src/server.js +2 -0
- package/templates/skills/webspresso-usage/SKILL.md +5 -2
package/src/file-router.js
CHANGED
|
@@ -140,6 +140,69 @@ function extractMethodFromFilename(filename) {
|
|
|
140
140
|
return result;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Whether `load()` return values for `stylesheets` and `scripts` are promoted to
|
|
145
|
+
* `pageHead` in Nunjucks (see `createApp({ pageAssets })`).
|
|
146
|
+
* @param {boolean|{enabled?: boolean, stylesheets?: boolean, scripts?: boolean}|null|undefined} raw
|
|
147
|
+
* @returns {{ enabled: boolean, stylesheets: boolean, scripts: boolean }}
|
|
148
|
+
*/
|
|
149
|
+
function resolvePageAssets(raw) {
|
|
150
|
+
if (raw === true) {
|
|
151
|
+
return { enabled: true, stylesheets: true, scripts: true };
|
|
152
|
+
}
|
|
153
|
+
if (raw == null || raw === false) {
|
|
154
|
+
return { enabled: false, stylesheets: false, scripts: false };
|
|
155
|
+
}
|
|
156
|
+
if (typeof raw === 'object') {
|
|
157
|
+
const on = raw.enabled !== false;
|
|
158
|
+
if (!on) {
|
|
159
|
+
return { enabled: false, stylesheets: false, scripts: false };
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
enabled: true,
|
|
163
|
+
stylesheets: raw.stylesheets !== false,
|
|
164
|
+
scripts: raw.scripts !== false,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return { enabled: false, stylesheets: false, scripts: false };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @param {unknown} v
|
|
172
|
+
* @returns {unknown[]}
|
|
173
|
+
*/
|
|
174
|
+
function toList(v) {
|
|
175
|
+
if (v == null) return [];
|
|
176
|
+
return Array.isArray(v) ? v : [v];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* @param {{ enabled: boolean, stylesheets: boolean, scripts: boolean }} cfg
|
|
181
|
+
* @param {Object} data
|
|
182
|
+
* @returns {{ data: Object, pageHead: { stylesheets: unknown[], scripts: unknown[] }|null, pageAssets: boolean }}
|
|
183
|
+
*/
|
|
184
|
+
function applyPageAssetsToTemplateData(cfg, data) {
|
|
185
|
+
if (!cfg || !cfg.enabled) {
|
|
186
|
+
return { data, pageHead: null, pageAssets: false };
|
|
187
|
+
}
|
|
188
|
+
const out = { ...data };
|
|
189
|
+
let styles = [];
|
|
190
|
+
let scriptItems = [];
|
|
191
|
+
if (cfg.stylesheets && Object.prototype.hasOwnProperty.call(out, 'stylesheets')) {
|
|
192
|
+
styles = toList(out.stylesheets);
|
|
193
|
+
delete out.stylesheets;
|
|
194
|
+
}
|
|
195
|
+
if (cfg.scripts && Object.prototype.hasOwnProperty.call(out, 'scripts')) {
|
|
196
|
+
scriptItems = toList(out.scripts);
|
|
197
|
+
delete out.scripts;
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
data: out,
|
|
201
|
+
pageHead: { stylesheets: styles, scripts: scriptItems },
|
|
202
|
+
pageAssets: true,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
143
206
|
/**
|
|
144
207
|
* Recursively scan a directory for files
|
|
145
208
|
* @param {string} dir - Directory to scan
|
|
@@ -439,6 +502,7 @@ function resolveMiddlewares(middlewareConfig, middlewareRegistry = {}) {
|
|
|
439
502
|
* @param {boolean} options.silent - Suppress console output
|
|
440
503
|
* @param {Object} options.db - Database instance (exposed as ctx.db in load/meta)
|
|
441
504
|
* @param {{ alpine?: boolean, swup?: boolean }} [options.clientRuntime] - Passed to Nunjucks as `clientRuntime` (default both false)
|
|
505
|
+
* @param {boolean|{enabled?: boolean, stylesheets?: boolean, scripts?: boolean}} [options.pageAssets] - If set, `load()` may return `stylesheets` / `scripts` promoted to `pageHead` in templates
|
|
442
506
|
* @returns {Array} Route metadata for plugins
|
|
443
507
|
*/
|
|
444
508
|
function mountPages(app, options) {
|
|
@@ -450,7 +514,9 @@ function mountPages(app, options) {
|
|
|
450
514
|
silent = false,
|
|
451
515
|
db = null,
|
|
452
516
|
clientRuntime: clientRuntimeOpt = null,
|
|
517
|
+
pageAssets: pageAssetsOpt = null,
|
|
453
518
|
} = options;
|
|
519
|
+
const pageAssetsResolved = resolvePageAssets(pageAssetsOpt);
|
|
454
520
|
const clientRuntime = clientRuntimeOpt && typeof clientRuntimeOpt === 'object'
|
|
455
521
|
? { alpine: !!clientRuntimeOpt.alpine, swup: !!clientRuntimeOpt.swup }
|
|
456
522
|
: { alpine: false, swup: false };
|
|
@@ -686,9 +752,9 @@ function mountPages(app, options) {
|
|
|
686
752
|
await executeHook(globalHooks, 'beforeRender', ctx);
|
|
687
753
|
await executeHook(routeHooks, 'beforeRender', ctx);
|
|
688
754
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
const
|
|
755
|
+
const pageAssetBundle = applyPageAssetsToTemplateData(pageAssetsResolved, ctx.data);
|
|
756
|
+
ctx.data = pageAssetBundle.data;
|
|
757
|
+
const renderContext = {
|
|
692
758
|
...ctx.data,
|
|
693
759
|
meta: ctx.meta,
|
|
694
760
|
locale: ctx.locale,
|
|
@@ -700,7 +766,15 @@ function mountPages(app, options) {
|
|
|
700
766
|
query: req.query,
|
|
701
767
|
params: req.params
|
|
702
768
|
}
|
|
703
|
-
}
|
|
769
|
+
};
|
|
770
|
+
if (pageAssetBundle.pageAssets) {
|
|
771
|
+
renderContext.pageAssets = true;
|
|
772
|
+
renderContext.pageHead = pageAssetBundle.pageHead;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Render the template
|
|
776
|
+
const templatePath = route.file.split(path.sep).join('/');
|
|
777
|
+
const html = nunjucks.render(templatePath, renderContext);
|
|
704
778
|
|
|
705
779
|
// Execute hooks: afterRender
|
|
706
780
|
ctx.html = html;
|
|
@@ -769,5 +843,7 @@ module.exports = {
|
|
|
769
843
|
resolveMiddlewares,
|
|
770
844
|
routeRegistrationMeta,
|
|
771
845
|
compareRouteRegistrationOrder,
|
|
846
|
+
resolvePageAssets,
|
|
847
|
+
applyPageAssetsToTemplateData,
|
|
772
848
|
};
|
|
773
849
|
|
package/src/server.js
CHANGED
|
@@ -261,6 +261,7 @@ function haltOnTimedout(req, res, next) {
|
|
|
261
261
|
* @param {Object} options.auth - Authentication manager instance (from createAuth)
|
|
262
262
|
* @param {Object} options.db - Database instance (exposed as ctx.db to plugins)
|
|
263
263
|
* @param {Object} [options.clientRuntime] - Optional client assets: `{ alpine?: boolean|object, swup?: boolean|object }`. Overridable by env `WEBSPRESSO_ALPINE` / `WEBSPRESSO_SWUP` (=1 or true). Serves `/__webspresso/client-runtime/*` when either flag is on.
|
|
264
|
+
* @param {boolean|{enabled?: boolean, stylesheets?: boolean, scripts?: boolean}} [options.pageAssets] - If truthy, route `load()` return values for `stylesheets` and `scripts` are reserved: removed from the root template context and passed as `pageHead` to Nunjucks, with `pageAssets: true` (for layout to emit `<link>` / `<script>`). Default off.
|
|
264
265
|
* @param {function(import('express').Express, Object): void} [options.setupRoutes] - Called after file routes and plugins, before 404 handler
|
|
265
266
|
* @returns {Object} { app, nunjucksEnv, pluginManager, authMiddleware }
|
|
266
267
|
*/
|
|
@@ -448,6 +449,7 @@ function createApp(options = {}) {
|
|
|
448
449
|
silent: isTest,
|
|
449
450
|
db: options.db ?? null,
|
|
450
451
|
clientRuntime,
|
|
452
|
+
pageAssets: options.pageAssets,
|
|
451
453
|
});
|
|
452
454
|
|
|
453
455
|
// Set route metadata in plugin manager
|
|
@@ -68,6 +68,7 @@ project/
|
|
|
68
68
|
| `timeout` | e.g. `'30s'` or `false` |
|
|
69
69
|
| `helmet` | `true` / `false` / object |
|
|
70
70
|
| `assets` | `{ version, manifestPath, prefix }` for `fsy.asset` / `fsy.css` / `fsy.js` |
|
|
71
|
+
| `pageAssets` | Opt-in **`true`** or **`{ enabled?, stylesheets?, scripts? }`**. When on, route **`load()`** may return reserved keys **`stylesheets`** (string or list; also `{ href, media? }` objects) and **`scripts`** (string, `{ src, defer?, async?, type? }`, or list). They are removed from the root Nunjucks context and passed as **`pageHead`** with **`pageAssets: true`**. The app layout must print them (see [`views/layout.njk`](../../../views/layout.njk) in the package). Default **off** — `stylesheets` / `scripts` in **`load()`** behave as normal data keys. |
|
|
71
72
|
| `clientRuntime` | Opt-in **`{ alpine?: boolean \| object, swup?: boolean \| object }`**. Serves **`/__webspresso/client-runtime/*`** (Alpine 3, swup 4 + Head + Scripts plugins + bootstrap). Template context **`clientRuntime`**; include [`views/partials/webspresso-client-runtime.njk`](../../../views/partials/webspresso-client-runtime.njk) and set **`<main id="swup">`** when swup is on. Env overrides: **`WEBSPRESSO_ALPINE`**, **`WEBSPRESSO_SWUP`** (`1` or `true`). Admin / dev dashboard HTML is unchanged (Mithril). Use **`data-no-swup`** on links for full page loads. HTMX is not used. |
|
|
72
73
|
| `auth` | `AuthManager` from **`createAuth()`** / **`quickAuth()`** (`webspresso/core/auth`). Mounts cookie parser + **`express-session`** + per-request **`authenticate`**; sets **`req.user`**, **`req.auth`**. Injects named route middleware **`auth`** and **`guest`** (overwrites same keys in `middlewares` if you passed both — avoid reusing those names for custom handlers). |
|
|
73
74
|
| `setupRoutes(app, ctx)` | **Register custom Express routes here** — runs **after** file routes and plugins’ `onRoutesReady`, **before** 404. **`ctx.clientRuntime`** is the resolved flags. **`ctx.authMiddleware`** is set when `auth` was passed (guards: `requireAuth`, `requireGuest`, `requireCan`, `requireVerified`, …). Do not rely on `app.get` *after* `createApp` returns unless routes are appended before the 404 middleware (see [`src/server.js`](../../../src/server.js)). |
|
|
@@ -91,8 +92,10 @@ project/
|
|
|
91
92
|
- **Route config:** `middleware: ['auth']` (must be logged in) or `['guest']` (logged-out only). For JSON APIs mounted in **`setupRoutes`**, use **`ctx.authMiddleware.requireAuth({ api: true })`** for 401 JSON instead of redirect.
|
|
92
93
|
- **Login page pitfall:** a **`pages/login.njk`** can register **before** `setupRoutes` and bypass **`requireGuest`**. Prefer login GET/POST in **`setupRoutes`** with templates under **`views/`** only, or omit **`pages/login.njk`** — see [`tests/e2e/auth.spec.js`](../../../tests/e2e/auth.spec.js).
|
|
93
94
|
- **Admin panel** uses a **separate** session (`req.session.adminUser`, `/_admin/api/auth/*`); it does **not** replace **`createApp({ auth })`** for site users.
|
|
95
|
+
- **Site users in the admin UI (`userManagement`):** Opt-in on **`adminPanelPlugin`**. Set **`userManagement: { enabled: true, model: 'User', fields?: { ... } }`** so the SPA shows **Users** (routes like **`/_admin/users`**, **`/_admin/users/new`**, **`/_admin/users/:id/edit`** — same Mithril shell as the rest of the panel). The **`model`** must be the ORM model your site auth uses (e.g. **`quickAuth({ userModel: 'User', ... })`** / **`createAuth`** adapters reading the same table). Pass **`auth: authManager`** with the **same** **`AuthManager`** instance as **`createApp({ auth: authManager })`** when you want **Active Sessions** / revoke APIs (**`rememberTokens`** / **`remember_me`**); without **`auth`**, list/create/update/delete users still work via **`db.getRepository(model)`**, but session endpoints return empty or “not enabled”.
|
|
96
|
+
- **Wiring:** `plugins: [ adminPanelPlugin({ db, auth: authManager, userManagement: { enabled: true, model: 'User' } }) ]` alongside `createApp({ ..., auth: authManager })`. Admin staff log in at **`/_admin`**; end users use your normal site login — two different cookies/sessions.
|
|
94
97
|
|
|
95
|
-
Longer narrative: **[`doc/index.html#authentication`](../../../doc/index.html#authentication)** · README **Authentication (session)**.
|
|
98
|
+
Longer narrative: **[`doc/index.html#authentication`](../../../doc/index.html#authentication)** · **[`#admin-user-management`](../../../doc/index.html#admin-user-management)** · README **Authentication (session)** and **Admin Panel Plugin**.
|
|
96
99
|
|
|
97
100
|
---
|
|
98
101
|
|
|
@@ -200,7 +203,7 @@ Pass **`db`** into **`createApp({ db })`** so **`ctx.db`** works in pages and pl
|
|
|
200
203
|
| `dashboardPlugin` | Dev route `/_webspresso` — route list |
|
|
201
204
|
| `sitemapPlugin` | `/sitemap.xml`, robots; optional DB-driven URLs |
|
|
202
205
|
| `analyticsPlugin` | GA / GTM / Yandex / Bing / Facebook — `fsy` helpers |
|
|
203
|
-
| `adminPanelPlugin` | SPA admin CRUD — needs
|
|
206
|
+
| `adminPanelPlugin` | SPA admin CRUD — needs **`db`**; optional **`uploadUrl`** (or infer from **`uploadPlugin`**); optional **`userManagement: { enabled, model, fields }`** + **`auth`** (same **`AuthManager`** as **`createApp({ auth })`**) for site-user CRUD + remember-me session UI — see **Session authentication** above |
|
|
204
207
|
| `uploadPlugin` | `POST` multipart (`multer`), `createLocalFileProvider` or custom `provider`; set **`mimeAllowlist`** / **`maxBytes`** in production |
|
|
205
208
|
| `siteAnalyticsPlugin` | Self-hosted page views + admin charts |
|
|
206
209
|
| `auditLogPlugin` | Admin mutation audit trail |
|