webspresso 0.0.45 → 0.0.47
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 +14 -0
- package/package.json +1 -1
- package/plugins/admin-panel/core/admin-module.js +146 -0
- package/plugins/admin-panel/index.js +23 -2
- package/plugins/site-analytics/index.js +31 -32
- package/src/file-router.js +3 -1
- package/src/plugin-manager.js +1 -0
- package/src/server.js +4 -1
package/README.md
CHANGED
|
@@ -254,6 +254,7 @@ Creates and configures the Express app.
|
|
|
254
254
|
- `pagesDir` (required): Path to pages directory
|
|
255
255
|
- `viewsDir` (optional): Path to views/layouts directory
|
|
256
256
|
- `publicDir` (optional): Path to public/static directory
|
|
257
|
+
- `db` (optional): Database instance — exposed as `ctx.db` in plugin hooks (`register`, `onRoutesReady`) and in page `load`/`meta` functions
|
|
257
258
|
- `logging` (optional): Enable request logging (default: true in development)
|
|
258
259
|
- `helmet` (optional): Helmet security configuration
|
|
259
260
|
- `true` or `undefined`: Use default secure configuration
|
|
@@ -633,6 +634,11 @@ const myPlugin = {
|
|
|
633
634
|
// Access Express app
|
|
634
635
|
ctx.app.use((req, res, next) => next());
|
|
635
636
|
|
|
637
|
+
// Access database (when createApp({ db }) is used)
|
|
638
|
+
if (ctx.db) {
|
|
639
|
+
// Use ctx.db.getRepository('Model'), ctx.db.knex, etc.
|
|
640
|
+
}
|
|
641
|
+
|
|
636
642
|
// Add template helpers
|
|
637
643
|
ctx.addHelper('myHelper', () => 'Hello!');
|
|
638
644
|
|
|
@@ -645,6 +651,7 @@ const myPlugin = {
|
|
|
645
651
|
|
|
646
652
|
// Called after all routes are mounted
|
|
647
653
|
onRoutesReady(ctx) {
|
|
654
|
+
// ctx.db available when createApp({ db }) is used
|
|
648
655
|
// Access route metadata
|
|
649
656
|
console.log('Routes:', ctx.routes);
|
|
650
657
|
|
|
@@ -777,6 +784,11 @@ module.exports = {
|
|
|
777
784
|
|
|
778
785
|
// Load data for SSR
|
|
779
786
|
async load(req, ctx) {
|
|
787
|
+
// ctx.db is available when createApp({ db }) is used
|
|
788
|
+
if (ctx.db) {
|
|
789
|
+
const posts = await ctx.db.getRepository('Post').query().limit(10);
|
|
790
|
+
return { posts };
|
|
791
|
+
}
|
|
780
792
|
return { tools: await fetchTools() };
|
|
781
793
|
},
|
|
782
794
|
|
|
@@ -1043,6 +1055,8 @@ const db = createDatabase({
|
|
|
1043
1055
|
const UserRepo = db.getRepository('User');
|
|
1044
1056
|
```
|
|
1045
1057
|
|
|
1058
|
+
Pass `db` to `createApp({ db })` to expose it as `ctx.db` in plugin hooks and page `load`/`meta` functions.
|
|
1059
|
+
|
|
1046
1060
|
**Model File Structure:**
|
|
1047
1061
|
- Place model files in `models/` directory (or custom path via `config.models`)
|
|
1048
1062
|
- Each file should export a model defined with `defineModel()`
|
package/package.json
CHANGED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin Module Registration System
|
|
3
|
+
* Declarative API for registering admin pages, menu items, API routes, and widgets
|
|
4
|
+
* @module plugins/admin-panel/core/admin-module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Register an admin module with all its components in a single declarative call
|
|
9
|
+
* @param {Object} config - Module configuration
|
|
10
|
+
* @param {string} config.id - Unique module identifier (required)
|
|
11
|
+
* @param {Array} [config.pages] - Custom admin pages
|
|
12
|
+
* @param {Array} [config.menu] - Sidebar menu items
|
|
13
|
+
* @param {Object} [config.menuGroups] - Menu group definitions
|
|
14
|
+
* @param {Object} [config.api] - API endpoint definitions
|
|
15
|
+
* @param {Array} [config.widgets] - Dashboard widgets
|
|
16
|
+
* @param {Object} deps - Internal dependencies injected by admin-panel
|
|
17
|
+
* @param {Object} deps.registry - AdminRegistry instance
|
|
18
|
+
* @param {string} deps.adminPath - Admin panel base path
|
|
19
|
+
* @param {Object} deps.ctx - Plugin context
|
|
20
|
+
* @param {Function} deps.requireAuth - Auth middleware
|
|
21
|
+
* @param {Function} deps.optionalAuth - Optional auth middleware
|
|
22
|
+
* @param {Function} deps.serveAdminPanel - SPA HTML handler
|
|
23
|
+
*/
|
|
24
|
+
function registerModule(config, deps) {
|
|
25
|
+
if (!config || typeof config !== 'object') {
|
|
26
|
+
throw new Error('registerModule requires a config object');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!config.id || typeof config.id !== 'string') {
|
|
30
|
+
throw new Error('registerModule requires a string "id" field');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const { registry, adminPath, ctx, requireAuth, optionalAuth, serveAdminPanel } = deps;
|
|
34
|
+
|
|
35
|
+
if (config.menuGroups) {
|
|
36
|
+
registerMenuGroups(config.menuGroups, registry);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (config.pages) {
|
|
40
|
+
registerPages(config.id, config.pages, { registry, adminPath, ctx, optionalAuth, serveAdminPanel });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (config.menu) {
|
|
44
|
+
registerMenuItems(config.menu, registry);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (config.api) {
|
|
48
|
+
registerApiRoutes(config.id, config.api, { adminPath, ctx, requireAuth, optionalAuth });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (config.widgets) {
|
|
52
|
+
registerWidgets(config.widgets, registry);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function registerPages(moduleId, pages, deps) {
|
|
57
|
+
if (!Array.isArray(pages)) {
|
|
58
|
+
throw new Error(`Module "${moduleId}": pages must be an array`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const { registry, adminPath, ctx, optionalAuth, serveAdminPanel } = deps;
|
|
62
|
+
|
|
63
|
+
for (const page of pages) {
|
|
64
|
+
const pageId = page.id || moduleId;
|
|
65
|
+
|
|
66
|
+
if (!page.title || !page.path) {
|
|
67
|
+
throw new Error(`Module "${moduleId}", page "${pageId}": requires title and path`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
registry.registerPage(pageId, {
|
|
71
|
+
title: page.title,
|
|
72
|
+
path: page.path,
|
|
73
|
+
icon: page.icon,
|
|
74
|
+
description: page.description,
|
|
75
|
+
permission: page.permission,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (page.component) {
|
|
79
|
+
registry.registerClientComponent(pageId, page.component);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
ctx.addRoute('get', `${adminPath}${page.path}`, optionalAuth, serveAdminPanel);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function registerMenuItems(menu, registry) {
|
|
87
|
+
if (!Array.isArray(menu)) {
|
|
88
|
+
throw new Error('menu must be an array');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const item of menu) {
|
|
92
|
+
registry.registerMenuItem(item);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function registerMenuGroups(menuGroups, registry) {
|
|
97
|
+
if (typeof menuGroups !== 'object' || Array.isArray(menuGroups)) {
|
|
98
|
+
throw new Error('menuGroups must be an object');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const [id, config] of Object.entries(menuGroups)) {
|
|
102
|
+
registry.registerMenuGroup(id, config);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function registerApiRoutes(moduleId, apiConfig, deps) {
|
|
107
|
+
if (typeof apiConfig !== 'object') {
|
|
108
|
+
throw new Error(`Module "${moduleId}": api must be an object`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const { adminPath, ctx, requireAuth, optionalAuth } = deps;
|
|
112
|
+
const prefix = apiConfig.prefix || '';
|
|
113
|
+
const defaultAuth = apiConfig.auth !== false;
|
|
114
|
+
|
|
115
|
+
if (!Array.isArray(apiConfig.routes)) {
|
|
116
|
+
throw new Error(`Module "${moduleId}": api.routes must be an array`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
for (const route of apiConfig.routes) {
|
|
120
|
+
if (!route.path || typeof route.handler !== 'function') {
|
|
121
|
+
throw new Error(`Module "${moduleId}": each API route requires path and handler`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const method = (route.method || 'get').toLowerCase();
|
|
125
|
+
const fullPath = `${adminPath}/api${prefix}${route.path}`;
|
|
126
|
+
const useAuth = route.auth !== undefined ? route.auth : defaultAuth;
|
|
127
|
+
const authMiddleware = useAuth ? requireAuth : optionalAuth;
|
|
128
|
+
|
|
129
|
+
ctx.addRoute(method, fullPath, authMiddleware, route.handler);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function registerWidgets(widgets, registry) {
|
|
134
|
+
if (!Array.isArray(widgets)) {
|
|
135
|
+
throw new Error('widgets must be an array');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (const widget of widgets) {
|
|
139
|
+
if (!widget.id) {
|
|
140
|
+
throw new Error('Each widget requires an id');
|
|
141
|
+
}
|
|
142
|
+
registry.registerWidget(widget.id, widget);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = { registerModule };
|
|
@@ -15,6 +15,7 @@ const { registerDashboardWidgets, generateDashboardComponent } = require('./modu
|
|
|
15
15
|
const { registerDefaultBulkActions, generateBulkActionsComponent } = require('./modules/bulk-actions');
|
|
16
16
|
const { registerDefaultPages, createCustomPageApiHandlers, generateCustomPageComponent } = require('./modules/custom-pages');
|
|
17
17
|
const { registerModelMenuItems, registerSystemMenuItems, generateMenuComponent } = require('./modules/menu');
|
|
18
|
+
const { registerModule } = require('./core/admin-module');
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Admin Panel Plugin Factory
|
|
@@ -64,10 +65,15 @@ function adminPanelPlugin(options = {}) {
|
|
|
64
65
|
version: '2.0.0',
|
|
65
66
|
description: 'Modular admin panel for Webspresso with extensions support',
|
|
66
67
|
|
|
67
|
-
// CSP requirements for
|
|
68
|
+
// CSP requirements for admin panel scripts
|
|
68
69
|
csp: {
|
|
69
70
|
styleSrc: ['https://cdn.quilljs.com'],
|
|
70
|
-
scriptSrc: [
|
|
71
|
+
scriptSrc: [
|
|
72
|
+
'https://cdn.quilljs.com',
|
|
73
|
+
'https://unpkg.com',
|
|
74
|
+
'https://cdn.tailwindcss.com',
|
|
75
|
+
],
|
|
76
|
+
connectSrc: ['https://unpkg.com', 'https://cdn.tailwindcss.com'],
|
|
71
77
|
},
|
|
72
78
|
enabled,
|
|
73
79
|
registry, // Expose registry for external configuration
|
|
@@ -78,6 +84,20 @@ function adminPanelPlugin(options = {}) {
|
|
|
78
84
|
serveAdminPanel,
|
|
79
85
|
requireAuth,
|
|
80
86
|
optionalAuth,
|
|
87
|
+
registerModule(config) {
|
|
88
|
+
if (!this._ctx) {
|
|
89
|
+
throw new Error('registerModule can only be called during or after onRoutesReady');
|
|
90
|
+
}
|
|
91
|
+
return registerModule(config, {
|
|
92
|
+
registry,
|
|
93
|
+
adminPath,
|
|
94
|
+
ctx: this._ctx,
|
|
95
|
+
requireAuth,
|
|
96
|
+
optionalAuth,
|
|
97
|
+
serveAdminPanel,
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
_ctx: null,
|
|
81
101
|
},
|
|
82
102
|
|
|
83
103
|
/**
|
|
@@ -101,6 +121,7 @@ function adminPanelPlugin(options = {}) {
|
|
|
101
121
|
return;
|
|
102
122
|
}
|
|
103
123
|
|
|
124
|
+
this.api._ctx = ctx;
|
|
104
125
|
const { app } = ctx;
|
|
105
126
|
|
|
106
127
|
// Create and register AdminUser model
|
|
@@ -37,6 +37,7 @@ function siteAnalyticsPlugin(options = {}) {
|
|
|
37
37
|
|
|
38
38
|
csp: {
|
|
39
39
|
scriptSrc: ['https://cdn.jsdelivr.net'],
|
|
40
|
+
connectSrc: ['https://cdn.jsdelivr.net'],
|
|
40
41
|
},
|
|
41
42
|
|
|
42
43
|
register(ctx) {
|
|
@@ -59,42 +60,40 @@ function siteAnalyticsPlugin(options = {}) {
|
|
|
59
60
|
return;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
const registry = adminApi.getRegistry();
|
|
63
|
-
const adminPath = adminApi.getAdminPath();
|
|
64
|
-
const { requireAuth } = adminApi;
|
|
65
63
|
const knex = db.knex || db;
|
|
66
|
-
|
|
67
|
-
// Register admin page
|
|
68
|
-
registry.registerPage('analytics', {
|
|
69
|
-
title: 'Analytics',
|
|
70
|
-
path: '/analytics',
|
|
71
|
-
icon: 'chart',
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
// Register menu item
|
|
75
|
-
registry.registerMenuItem({
|
|
76
|
-
id: 'analytics',
|
|
77
|
-
label: 'Analytics',
|
|
78
|
-
path: '/analytics',
|
|
79
|
-
icon: 'chart',
|
|
80
|
-
order: 2,
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
// Register client-side component
|
|
84
|
-
registry.registerClientComponent('analytics', generateAnalyticsComponent());
|
|
85
|
-
|
|
86
|
-
// API routes
|
|
87
64
|
const handlers = createAnalyticsApiHandlers({ knex, tableName });
|
|
88
65
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
ctx.addRoute('get', `${adminPath}/api/analytics/top-pages`, requireAuth, handlers.getTopPages);
|
|
92
|
-
ctx.addRoute('get', `${adminPath}/api/analytics/bot-activity`, requireAuth, handlers.getBotActivity);
|
|
93
|
-
ctx.addRoute('get', `${adminPath}/api/analytics/countries`, requireAuth, handlers.getCountries);
|
|
94
|
-
ctx.addRoute('get', `${adminPath}/api/analytics/recent`, requireAuth, handlers.getRecent);
|
|
66
|
+
adminApi.registerModule({
|
|
67
|
+
id: 'analytics',
|
|
95
68
|
|
|
96
|
-
|
|
97
|
-
|
|
69
|
+
pages: [{
|
|
70
|
+
id: 'analytics',
|
|
71
|
+
title: 'Analytics',
|
|
72
|
+
path: '/analytics',
|
|
73
|
+
icon: 'chart',
|
|
74
|
+
component: generateAnalyticsComponent(),
|
|
75
|
+
}],
|
|
76
|
+
|
|
77
|
+
menu: [{
|
|
78
|
+
id: 'analytics',
|
|
79
|
+
label: 'Analytics',
|
|
80
|
+
path: '/analytics',
|
|
81
|
+
icon: 'chart',
|
|
82
|
+
order: 2,
|
|
83
|
+
}],
|
|
84
|
+
|
|
85
|
+
api: {
|
|
86
|
+
prefix: '/analytics',
|
|
87
|
+
routes: [
|
|
88
|
+
{ method: 'get', path: '/stats', handler: handlers.getStats },
|
|
89
|
+
{ method: 'get', path: '/views-over-time', handler: handlers.getViewsOverTime },
|
|
90
|
+
{ method: 'get', path: '/top-pages', handler: handlers.getTopPages },
|
|
91
|
+
{ method: 'get', path: '/bot-activity', handler: handlers.getBotActivity },
|
|
92
|
+
{ method: 'get', path: '/countries', handler: handlers.getCountries },
|
|
93
|
+
{ method: 'get', path: '/recent', handler: handlers.getRecent },
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
});
|
|
98
97
|
},
|
|
99
98
|
};
|
|
100
99
|
}
|
package/src/file-router.js
CHANGED
|
@@ -312,10 +312,11 @@ function resolveMiddlewares(middlewareConfig, middlewareRegistry = {}) {
|
|
|
312
312
|
* @param {Object} options.middlewares - Named middleware registry
|
|
313
313
|
* @param {Object} options.pluginManager - Plugin manager instance
|
|
314
314
|
* @param {boolean} options.silent - Suppress console output
|
|
315
|
+
* @param {Object} options.db - Database instance (exposed as ctx.db in load/meta)
|
|
315
316
|
* @returns {Array} Route metadata for plugins
|
|
316
317
|
*/
|
|
317
318
|
function mountPages(app, options) {
|
|
318
|
-
const { pagesDir, nunjucks, middlewares = {}, pluginManager = null, silent = false } = options;
|
|
319
|
+
const { pagesDir, nunjucks, middlewares = {}, pluginManager = null, silent = false, db = null } = options;
|
|
319
320
|
const isDev = process.env.NODE_ENV !== 'production';
|
|
320
321
|
const log = silent ? () => {} : console.log.bind(console);
|
|
321
322
|
|
|
@@ -453,6 +454,7 @@ function mountPages(app, options) {
|
|
|
453
454
|
const ctx = {
|
|
454
455
|
req,
|
|
455
456
|
res,
|
|
457
|
+
db,
|
|
456
458
|
path: route.routePath,
|
|
457
459
|
file: route.file,
|
|
458
460
|
routeDir: route.routeDir,
|
package/src/plugin-manager.js
CHANGED
package/src/server.js
CHANGED
|
@@ -164,6 +164,7 @@ function haltOnTimedout(req, res, next) {
|
|
|
164
164
|
* @param {Function|string} options.errorPages.timeout - Custom timeout handler or template path
|
|
165
165
|
* @param {string|boolean} options.timeout - Request timeout (default: '30s', false to disable)
|
|
166
166
|
* @param {Object} options.auth - Authentication manager instance (from createAuth)
|
|
167
|
+
* @param {Object} options.db - Database instance (exposed as ctx.db to plugins)
|
|
167
168
|
* @returns {Object} { app, nunjucksEnv, pluginManager, authMiddleware }
|
|
168
169
|
*/
|
|
169
170
|
function createApp(options = {}) {
|
|
@@ -340,7 +341,8 @@ function createApp(options = {}) {
|
|
|
340
341
|
nunjucks: nunjucksEnv,
|
|
341
342
|
middlewares,
|
|
342
343
|
pluginManager,
|
|
343
|
-
silent: isTest
|
|
344
|
+
silent: isTest,
|
|
345
|
+
db: options.db ?? null
|
|
344
346
|
});
|
|
345
347
|
|
|
346
348
|
// Set route metadata in plugin manager
|
|
@@ -354,6 +356,7 @@ function createApp(options = {}) {
|
|
|
354
356
|
app,
|
|
355
357
|
nunjucksEnv,
|
|
356
358
|
options,
|
|
359
|
+
db: options.db ?? null,
|
|
357
360
|
routes: pluginManager.routes,
|
|
358
361
|
usePlugin: (n) => pluginManager.getPluginAPI(n),
|
|
359
362
|
addHelper: (n, fn) => pluginManager.registeredHelpers.set(n, fn),
|