webspresso 0.0.96 → 0.0.98
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 +153 -5
- package/.agents/ARCHITECTURE.md +10 -0
- package/.agents/CLI.md +195 -1
- package/.agents/PLUGINS.md +153 -9
- package/.agents/QUEUE.md +43 -0
- package/.agents/skills/webspresso-admin-and-plugins/SKILL.md +122 -0
- package/.agents/skills/webspresso-framework/SKILL.md +56 -0
- package/.agents/skills/webspresso-orm-and-database/SKILL.md +119 -0
- package/.agents/skills/webspresso-routing-and-api/SKILL.md +114 -0
- package/.agents/skills/webspresso-services/SKILL.md +101 -0
- package/bin/commands/add-esbuild.js +80 -0
- package/bin/commands/add-tailwind.js +76 -31
- package/bin/commands/admin-password.js +2 -1
- package/bin/commands/admin-setup.js +1 -1
- package/bin/commands/agents-init.js +51 -11
- package/bin/commands/analytics-prune.js +57 -0
- package/bin/commands/api.js +207 -15
- package/bin/commands/audit-prune.js +1 -1
- package/bin/commands/build.js +99 -0
- package/bin/commands/dev.js +64 -35
- package/bin/commands/doctor.js +26 -0
- package/bin/commands/email-prune.js +47 -0
- package/bin/commands/favicon-generate.js +1 -1
- package/bin/commands/make-job.js +86 -0
- package/bin/commands/make-model.js +250 -0
- package/bin/commands/new.js +45 -28
- package/bin/commands/openapi.js +102 -0
- package/bin/commands/orm-map.js +5 -4
- package/bin/commands/page.js +6 -3
- package/bin/commands/polar-migrate.js +1 -1
- package/bin/commands/queue-prune.js +59 -0
- package/bin/commands/routes.js +52 -0
- package/bin/commands/seed.js +1 -1
- package/bin/commands/service.js +6 -3
- package/bin/commands/typegen.js +106 -0
- package/bin/commands/upload-prune.js +79 -0
- package/bin/webspresso.js +22 -0
- package/core/applySchema.js +13 -6
- package/core/auth/hash.js +31 -17
- package/core/compression/middleware.js +23 -17
- package/core/openapi/build-from-api-routes.js +160 -29
- package/core/orm/cache/fingerprint.js +15 -1
- package/core/orm/cache/memory-provider.js +27 -2
- package/core/orm/index.js +12 -7
- package/core/orm/model.js +49 -7
- package/core/queue/adapters/database.js +94 -0
- package/core/queue/adapters/memory.js +62 -0
- package/core/queue/adapters/redis.js +80 -0
- package/core/queue/manager.js +123 -0
- package/core/ssr/stream.js +11 -0
- package/index.d.ts +660 -10
- package/index.js +44 -96
- package/package.json +32 -12
- package/plugins/admin-panel/api.js +279 -17
- package/plugins/admin-panel/app.js +124 -31
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +314 -9
- package/plugins/admin-panel/client/parts/04-field-renderers.js +264 -67
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +10 -5
- package/plugins/admin-panel/client/parts/06-login-setup-forms.js +119 -28
- package/plugins/admin-panel/client/parts/07-model-list.js +49 -8
- package/plugins/admin-panel/client/parts/08-record-list.js +243 -23
- package/plugins/admin-panel/client/parts/09-record-form.js +59 -12
- package/plugins/admin-panel/client/parts/10-export-registry.js +23 -0
- package/plugins/admin-panel/core/admin-module.js +13 -7
- package/plugins/admin-panel/core/api-extensions.js +34 -2
- package/plugins/admin-panel/core/registry.js +96 -16
- package/plugins/admin-panel/index.js +96 -0
- package/plugins/admin-panel/lib/login-rate-limiter.js +119 -0
- package/plugins/admin-panel/modules/admin-users.js +16 -1
- package/plugins/admin-panel/modules/bulk-actions.js +13 -5
- package/plugins/admin-panel/modules/custom-pages.js +492 -3
- package/plugins/admin-panel/modules/dashboard.js +283 -0
- package/plugins/admin-panel/modules/menu.js +56 -18
- package/plugins/admin-panel/modules/user-management.js +24 -6
- package/plugins/audit-log/admin-component.js +11 -0
- package/plugins/audit-log/api-handlers.js +34 -0
- package/plugins/audit-log/index.js +129 -3
- package/plugins/audit-log/purge.js +60 -10
- package/plugins/basic-auth/index.js +3 -1
- package/plugins/content/client/inline-edit.js +12 -12
- package/plugins/cors.js +6 -7
- package/plugins/dashboard/app.js +152 -27
- package/plugins/dashboard/index.js +143 -39
- package/plugins/dashboard/styles.js +78 -0
- package/plugins/data-exchange/export-xlsx.js +9 -2
- package/plugins/data-exchange/import.js +9 -2
- package/plugins/data-exchange/index.js +6 -3
- package/plugins/email/index.js +132 -2
- package/plugins/email/purge.js +60 -10
- package/plugins/file-manager/core.js +1 -1
- package/plugins/file-manager/security.js +33 -23
- package/plugins/index.js +49 -87
- package/plugins/mcp/transports/sse.js +4 -2
- package/plugins/orm-cache-admin/index.js +4 -2
- package/plugins/queue/index.js +11 -0
- package/plugins/schema-explorer/admin-component.js +714 -0
- package/plugins/schema-explorer/relations-graph.js +205 -0
- package/plugins/schema-explorer.js +248 -69
- package/plugins/site-analytics/admin-component.js +30 -8
- package/plugins/site-analytics/api-handlers.js +42 -0
- package/plugins/site-analytics/index.js +147 -2
- package/plugins/site-analytics/purge.js +168 -0
- package/plugins/swagger.js +261 -13
- package/plugins/upload/index.js +26 -0
- package/plugins/upload/orphan-pruner.js +292 -0
- package/src/api/api-loader.js +3 -1
- package/src/bundle/index.js +517 -0
- package/src/bundle/manifest.js +165 -0
- package/src/dev/banner.js +296 -0
- package/src/dev/colors.js +70 -0
- package/src/dev/index.js +62 -0
- package/src/dev/profiler.js +354 -0
- package/src/dev/typegen.js +497 -0
- package/src/dev-reload/index.js +611 -0
- package/src/file-router.js +213 -63
- package/src/helpers.js +958 -98
- package/src/njk-frontmatter.js +12 -0
- package/src/pages/page-loader.js +61 -11
- package/src/server.js +629 -63
- package/src/services/discovery.js +6 -1
- package/src/services/index.js +6 -13
|
@@ -16,33 +16,141 @@ const { purgeAuditLogs } = require('./purge');
|
|
|
16
16
|
* @param {string} [options.tableName='audit_logs']
|
|
17
17
|
* @param {boolean} [options.includeDefaultPage=true] - Register Mithril list page + menu
|
|
18
18
|
* @param {string} [options.apiPrefix='/audit-logs'] - GET list under admin API
|
|
19
|
+
* @param {Object|boolean} [options.retention] - Retention policy configuration (set false to disable)
|
|
20
|
+
* @param {number} [options.retention.days=90] - Days to retain audit logs
|
|
21
|
+
* @param {number} [options.retention.intervalMs=86400000] - Auto-cleanup timer frequency (24h)
|
|
22
|
+
* @param {boolean} [options.retention.runOnStart=true] - Run initial purge shortly after startup
|
|
23
|
+
* @param {number} [options.retentionDays=90] - Shorthand for retention.days
|
|
24
|
+
* @param {number} [options.cleanupIntervalMs=86400000] - Shorthand for retention.intervalMs
|
|
25
|
+
* @param {boolean} [options.autoCleanup=true] - Enable automatic background cleanup
|
|
26
|
+
* @param {boolean} [options.cleanupOnStart=true] - Run initial purge on start
|
|
27
|
+
* @param {number} [options.cleanupStartDelayMs] - Delay in ms before start purge
|
|
19
28
|
*/
|
|
20
29
|
function auditLogPlugin(options = {}) {
|
|
21
30
|
const {
|
|
22
31
|
db,
|
|
32
|
+
title,
|
|
33
|
+
adminTitle,
|
|
34
|
+
pageTitle: customPageTitle,
|
|
23
35
|
adminPath: adminPathOpt,
|
|
24
36
|
tableName = 'audit_logs',
|
|
25
37
|
includeDefaultPage = true,
|
|
26
38
|
apiPrefix = '/audit-logs',
|
|
27
39
|
} = options;
|
|
28
40
|
|
|
41
|
+
const pageTitle = title || adminTitle || customPageTitle || 'Audit log';
|
|
42
|
+
|
|
29
43
|
if (!db) {
|
|
30
44
|
throw new Error('audit-log plugin requires a database instance. Pass `db` in options.');
|
|
31
45
|
}
|
|
32
46
|
|
|
33
47
|
const knex = db.knex || db;
|
|
34
48
|
|
|
49
|
+
// Retention configuration
|
|
50
|
+
const retentionOption = options.retention;
|
|
51
|
+
const isRetentionDisabled =
|
|
52
|
+
retentionOption === false ||
|
|
53
|
+
(typeof retentionOption === 'object' && retentionOption.enabled === false) ||
|
|
54
|
+
options.retentionDays === null ||
|
|
55
|
+
options.retentionDays === 0;
|
|
56
|
+
|
|
57
|
+
const autoCleanupEnabled =
|
|
58
|
+
!isRetentionDisabled &&
|
|
59
|
+
options.autoCleanup !== false &&
|
|
60
|
+
(typeof retentionOption !== 'object' || retentionOption.autoCleanup !== false);
|
|
61
|
+
|
|
62
|
+
const retentionConfig = {
|
|
63
|
+
enabled: !isRetentionDisabled,
|
|
64
|
+
autoCleanup: autoCleanupEnabled,
|
|
65
|
+
days: (typeof options.retentionDays === 'number' && options.retentionDays > 0)
|
|
66
|
+
? options.retentionDays
|
|
67
|
+
: (typeof retentionOption === 'object' && retentionOption?.days ? retentionOption.days : 90),
|
|
68
|
+
intervalMs: (typeof options.cleanupIntervalMs === 'number' && options.cleanupIntervalMs > 0)
|
|
69
|
+
? options.cleanupIntervalMs
|
|
70
|
+
: (typeof retentionOption === 'object' && retentionOption?.intervalMs ? retentionOption.intervalMs : 24 * 60 * 60 * 1000),
|
|
71
|
+
runOnStart: (typeof retentionOption === 'object' && retentionOption?.runOnStart !== undefined)
|
|
72
|
+
? !!retentionOption.runOnStart
|
|
73
|
+
: (options.cleanupOnStart !== undefined ? !!options.cleanupOnStart : true),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
let cleanupInterval = null;
|
|
77
|
+
let cleanupStartTimeout = null;
|
|
78
|
+
|
|
79
|
+
async function runCleanup() {
|
|
80
|
+
try {
|
|
81
|
+
return await purgeAuditLogs(knex, {
|
|
82
|
+
tableName,
|
|
83
|
+
days: retentionConfig.days,
|
|
84
|
+
});
|
|
85
|
+
} catch (err) {
|
|
86
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
87
|
+
console.warn('[audit-log] Failed to run retention cleanup:', err.message);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function startCleanup(customIntervalMs) {
|
|
93
|
+
stopCleanup();
|
|
94
|
+
if (!retentionConfig.enabled) return;
|
|
95
|
+
if (typeof customIntervalMs === 'number' && customIntervalMs > 0) {
|
|
96
|
+
retentionConfig.intervalMs = customIntervalMs;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (retentionConfig.runOnStart) {
|
|
100
|
+
const delayMs = typeof options.cleanupStartDelayMs === 'number'
|
|
101
|
+
? options.cleanupStartDelayMs
|
|
102
|
+
: (process.env.NODE_ENV === 'test' ? 50 : 5000);
|
|
103
|
+
cleanupStartTimeout = setTimeout(() => {
|
|
104
|
+
runCleanup();
|
|
105
|
+
}, delayMs);
|
|
106
|
+
if (cleanupStartTimeout && cleanupStartTimeout.unref) {
|
|
107
|
+
cleanupStartTimeout.unref();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
cleanupInterval = setInterval(() => {
|
|
112
|
+
runCleanup();
|
|
113
|
+
}, retentionConfig.intervalMs);
|
|
114
|
+
if (cleanupInterval && cleanupInterval.unref) {
|
|
115
|
+
cleanupInterval.unref();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function stopCleanup() {
|
|
120
|
+
if (cleanupStartTimeout) {
|
|
121
|
+
clearTimeout(cleanupStartTimeout);
|
|
122
|
+
cleanupStartTimeout = null;
|
|
123
|
+
}
|
|
124
|
+
if (cleanupInterval) {
|
|
125
|
+
clearInterval(cleanupInterval);
|
|
126
|
+
cleanupInterval = null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
35
130
|
const handlers = createAuditLogHandlers({ knex, tableName });
|
|
36
131
|
|
|
37
132
|
return {
|
|
38
133
|
name: 'audit-log',
|
|
39
134
|
version: '1.0.0',
|
|
40
|
-
description: 'Audit trail for admin panel model CRUD API',
|
|
135
|
+
description: 'Audit trail for admin panel model CRUD API with retention cleanup',
|
|
41
136
|
dependencies: { 'admin-panel': '*' },
|
|
42
137
|
|
|
43
138
|
api: {
|
|
139
|
+
purge(opts = {}) {
|
|
140
|
+
return purgeAuditLogs(knex, {
|
|
141
|
+
tableName,
|
|
142
|
+
days: opts.days !== undefined ? opts.days : (opts.retentionDays !== undefined ? opts.retentionDays : retentionConfig.days),
|
|
143
|
+
olderThan: opts.olderThan,
|
|
144
|
+
...opts,
|
|
145
|
+
});
|
|
146
|
+
},
|
|
44
147
|
purgeAuditLogs: (kx, opts = {}) =>
|
|
45
|
-
purgeAuditLogs(kx || knex, { tableName: opts.tableName || tableName, olderThan: opts.olderThan }),
|
|
148
|
+
purgeAuditLogs(kx || knex, { tableName: opts.tableName || tableName, olderThan: opts.olderThan, days: opts.days, retentionDays: opts.retentionDays }),
|
|
149
|
+
startCleanup,
|
|
150
|
+
stopCleanup,
|
|
151
|
+
getRetentionConfig() {
|
|
152
|
+
return { ...retentionConfig };
|
|
153
|
+
},
|
|
46
154
|
queryLogs: (filters) => handlers.queryLogs(filters),
|
|
47
155
|
getMigrationTemplate: (name) => generateAuditLogsMigration(name || tableName),
|
|
48
156
|
},
|
|
@@ -50,6 +158,22 @@ function auditLogPlugin(options = {}) {
|
|
|
50
158
|
register(ctx) {
|
|
51
159
|
const adminPath = adminPathOpt || '/_admin';
|
|
52
160
|
ctx.app.use(createAuditMiddleware({ knex, adminPath, tableName }));
|
|
161
|
+
|
|
162
|
+
if (retentionConfig.enabled && autoCleanupEnabled) {
|
|
163
|
+
startCleanup();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return () => {
|
|
167
|
+
stopCleanup();
|
|
168
|
+
};
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
dispose() {
|
|
172
|
+
stopCleanup();
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
shutdown() {
|
|
176
|
+
stopCleanup();
|
|
53
177
|
},
|
|
54
178
|
|
|
55
179
|
onRoutesReady(ctx) {
|
|
@@ -61,11 +185,12 @@ function auditLogPlugin(options = {}) {
|
|
|
61
185
|
|
|
62
186
|
adminApi.registerModule({
|
|
63
187
|
id: 'audit-log',
|
|
188
|
+
title: pageTitle,
|
|
64
189
|
|
|
65
190
|
pages: includeDefaultPage
|
|
66
191
|
? [{
|
|
67
192
|
id: 'audit-log',
|
|
68
|
-
title:
|
|
193
|
+
title: pageTitle,
|
|
69
194
|
path: '/audit-log',
|
|
70
195
|
icon: 'database',
|
|
71
196
|
component: generateAuditLogComponent({ apiPrefix }),
|
|
@@ -86,6 +211,7 @@ function auditLogPlugin(options = {}) {
|
|
|
86
211
|
prefix: apiPrefix,
|
|
87
212
|
routes: [
|
|
88
213
|
{ method: 'get', path: '', handler: handlers.listHandler },
|
|
214
|
+
{ method: 'post', path: '/purge', handler: handlers.purgeHandler },
|
|
89
215
|
],
|
|
90
216
|
},
|
|
91
217
|
});
|
|
@@ -3,24 +3,74 @@
|
|
|
3
3
|
* @module plugins/audit-log/purge
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* @param {import('knex').Knex} knex
|
|
8
|
-
* @param {Object} options
|
|
10
|
+
* @param {Object} [options={}]
|
|
9
11
|
* @param {string} [options.tableName='audit_logs']
|
|
10
|
-
* @param {Date|string} options.olderThan - Rows with created_at strictly before this are removed
|
|
11
|
-
* @
|
|
12
|
+
* @param {Date|string|number} [options.olderThan] - Rows with created_at strictly before this are removed
|
|
13
|
+
* @param {number} [options.days] - Number of days of audit logs to keep
|
|
14
|
+
* @param {number} [options.retentionDays=90] - Alias for days
|
|
15
|
+
* @returns {Promise<number>} Deleted row count
|
|
12
16
|
*/
|
|
13
|
-
async function purgeAuditLogs(knex, options) {
|
|
17
|
+
async function purgeAuditLogs(knex, options = {}) {
|
|
18
|
+
if (!knex) {
|
|
19
|
+
throw new Error('purgeAuditLogs requires a database (knex) instance');
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
const tableName = options.tableName || 'audit_logs';
|
|
15
|
-
const olderThan = options.olderThan instanceof Date
|
|
16
|
-
? options.olderThan
|
|
17
|
-
: new Date(options.olderThan);
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
let olderThan;
|
|
25
|
+
if (options.olderThan !== undefined) {
|
|
26
|
+
olderThan = options.olderThan instanceof Date
|
|
27
|
+
? options.olderThan
|
|
28
|
+
: new Date(options.olderThan);
|
|
29
|
+
|
|
30
|
+
if (Number.isNaN(olderThan.getTime())) {
|
|
31
|
+
throw new Error('purgeAuditLogs: invalid olderThan date');
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
const rawDays = options.days !== undefined ? options.days : (options.retentionDays !== undefined ? options.retentionDays : 90);
|
|
35
|
+
const days = typeof rawDays === 'number' ? rawDays : Number(rawDays);
|
|
36
|
+
if (Number.isNaN(days) || days <= 0) {
|
|
37
|
+
throw new Error('purgeAuditLogs: days must be a positive number');
|
|
38
|
+
}
|
|
39
|
+
olderThan = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let hasTable = true;
|
|
43
|
+
if (knex.schema && typeof knex.schema.hasTable === 'function') {
|
|
44
|
+
try {
|
|
45
|
+
hasTable = await knex.schema.hasTable(tableName);
|
|
46
|
+
} catch {
|
|
47
|
+
hasTable = false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!hasTable) {
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const client = knex?.client?.config?.client;
|
|
56
|
+
const isSqlite = client === 'sqlite3' || client === 'better-sqlite3';
|
|
57
|
+
|
|
58
|
+
let query = knex(tableName);
|
|
59
|
+
if (isSqlite) {
|
|
60
|
+
const iso = olderThan.toISOString();
|
|
61
|
+
const cutoffMs = olderThan.getTime();
|
|
62
|
+
query = query.where((builder) => {
|
|
63
|
+
builder.whereRaw(
|
|
64
|
+
"((typeof(created_at) = 'integer' and created_at < ?) or (typeof(created_at) = 'text' and created_at < ?))",
|
|
65
|
+
[cutoffMs, iso]
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
} else {
|
|
69
|
+
query = query.where('created_at', '<', olderThan);
|
|
21
70
|
}
|
|
22
71
|
|
|
23
|
-
|
|
72
|
+
const deleted = await query.delete();
|
|
73
|
+
return Number(deleted) || 0;
|
|
24
74
|
}
|
|
25
75
|
|
|
26
76
|
module.exports = {
|
|
@@ -117,7 +117,9 @@ function sendUnauthorized(req, res, config) {
|
|
|
117
117
|
|
|
118
118
|
if (config.challenge !== false) {
|
|
119
119
|
const realm = String(config.realm || 'Restricted Area')
|
|
120
|
-
.split(/[\r\n]/)[0]
|
|
120
|
+
.split(/[\r\n\0]/)[0]
|
|
121
|
+
.replace(/[\t]/g, ' ')
|
|
122
|
+
.replace(/\\/g, '\\\\')
|
|
121
123
|
.replace(/"/g, '\\"');
|
|
122
124
|
res.setHeader('WWW-Authenticate', `Basic realm="${realm}"`);
|
|
123
125
|
}
|
|
@@ -248,9 +248,14 @@
|
|
|
248
248
|
el.textContent = '';
|
|
249
249
|
return;
|
|
250
250
|
}
|
|
251
|
+
if (typeof window.DOMPurify !== 'undefined' && typeof window.DOMPurify.sanitize === 'function') {
|
|
252
|
+
el.innerHTML = window.DOMPurify.sanitize(htmlStr);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
251
255
|
try {
|
|
252
|
-
var
|
|
253
|
-
|
|
256
|
+
var template = document.createElement('template');
|
|
257
|
+
template.innerHTML = htmlStr;
|
|
258
|
+
var content = template.content;
|
|
254
259
|
var allowedTags = {
|
|
255
260
|
p: 1, br: 1, b: 1, i: 1, strong: 1, em: 1, u: 1, s: 1, strike: 1,
|
|
256
261
|
h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1, ul: 1, ol: 1, li: 1,
|
|
@@ -260,14 +265,14 @@
|
|
|
260
265
|
var allowedAttrs = {
|
|
261
266
|
href: 1, title: 1, target: 1, rel: 1, class: 1, id: 1, src: 1, alt: 1, width: 1, height: 1
|
|
262
267
|
};
|
|
263
|
-
var allNodes =
|
|
268
|
+
var allNodes = content.querySelectorAll('*');
|
|
264
269
|
for (var i = allNodes.length - 1; i >= 0; i--) {
|
|
265
270
|
var node = allNodes[i];
|
|
266
271
|
var tag = node.tagName.toLowerCase();
|
|
267
272
|
if (!allowedTags[tag]) {
|
|
268
273
|
if (node.parentNode) {
|
|
269
274
|
node.parentNode.removeChild(node);
|
|
270
|
-
} else {
|
|
275
|
+
} else if (typeof node.remove === 'function') {
|
|
271
276
|
node.remove();
|
|
272
277
|
}
|
|
273
278
|
continue;
|
|
@@ -275,7 +280,7 @@
|
|
|
275
280
|
var attrs = Array.prototype.slice.call(node.attributes);
|
|
276
281
|
for (var j = 0; j < attrs.length; j++) {
|
|
277
282
|
var attrName = attrs[j].name.toLowerCase();
|
|
278
|
-
var attrVal = attrs[j].value.toLowerCase().replace(/[\x00-\x20\s]+/g, '').trim();
|
|
283
|
+
var attrVal = (attrs[j].value || '').toLowerCase().replace(/[\x00-\x20\s]+/g, '').trim();
|
|
279
284
|
if (attrName.indexOf('on') === 0 || !allowedAttrs[attrName]) {
|
|
280
285
|
node.removeAttribute(attrs[j].name);
|
|
281
286
|
} else if ((attrName === 'href' || attrName === 'src' || attrName === 'action' || attrName === 'formaction' || attrName === 'xlink:href') &&
|
|
@@ -287,13 +292,8 @@
|
|
|
287
292
|
}
|
|
288
293
|
}
|
|
289
294
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
el.removeChild(el.firstChild);
|
|
293
|
-
}
|
|
294
|
-
while (doc.body.firstChild) {
|
|
295
|
-
el.appendChild(doc.body.firstChild);
|
|
296
|
-
}
|
|
295
|
+
el.textContent = '';
|
|
296
|
+
el.appendChild(content.cloneNode(true));
|
|
297
297
|
} catch (e) {
|
|
298
298
|
el.textContent = htmlStr;
|
|
299
299
|
}
|
package/plugins/cors.js
CHANGED
|
@@ -101,15 +101,14 @@ function createCorsMiddleware(options = {}) {
|
|
|
101
101
|
return next(err);
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
if (allowedOrigin) {
|
|
104
|
+
if (allowedOrigin && allowedOrigin !== '*') {
|
|
105
105
|
res.setHeader('Access-Control-Allow-Origin', allowedOrigin);
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
res.setHeader('Vary', 'Origin');
|
|
107
|
+
if (config.credentials) {
|
|
108
|
+
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
|
108
109
|
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (config.credentials && allowedOrigin && allowedOrigin !== '*') {
|
|
112
|
-
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
|
110
|
+
} else if (allowedOrigin === '*' && !config.credentials) {
|
|
111
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
113
112
|
}
|
|
114
113
|
|
|
115
114
|
if (exposedHeadersStr) {
|
package/plugins/dashboard/app.js
CHANGED
|
@@ -10,6 +10,8 @@ const state = {
|
|
|
10
10
|
filter: 'all',
|
|
11
11
|
search: '',
|
|
12
12
|
routes: window.__DASHBOARD_DATA__.routes || [],
|
|
13
|
+
models: window.__DASHBOARD_DATA__.models || [],
|
|
14
|
+
services: window.__DASHBOARD_DATA__.services || [],
|
|
13
15
|
plugins: window.__DASHBOARD_DATA__.plugins || [],
|
|
14
16
|
config: window.__DASHBOARD_DATA__.config || {}
|
|
15
17
|
};
|
|
@@ -29,18 +31,27 @@ const Icons = {
|
|
|
29
31
|
// Components
|
|
30
32
|
const Header = {
|
|
31
33
|
view: () => m('.header', [
|
|
32
|
-
m('
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
m('div', [
|
|
35
|
+
m('h1', [
|
|
36
|
+
Icons.logo(),
|
|
37
|
+
'Webspresso Dashboard'
|
|
38
|
+
]),
|
|
39
|
+
m('span.dev-badge', 'development')
|
|
35
40
|
]),
|
|
36
|
-
m('
|
|
41
|
+
m('.header-actions', [
|
|
42
|
+
m('a.action-link', { href: '/_admin', target: '_blank', rel: 'noopener' }, '⚙️ Admin Panel ↗'),
|
|
43
|
+
m('a.action-link', { href: '/_swagger', target: '_blank', rel: 'noopener' }, '📖 API Docs ↗'),
|
|
44
|
+
m('a.action-link', { href: '/_schema', target: '_blank', rel: 'noopener' }, '🗂️ Schema Explorer ↗')
|
|
45
|
+
])
|
|
37
46
|
])
|
|
38
47
|
};
|
|
39
48
|
|
|
40
49
|
const Tabs = {
|
|
41
50
|
view: () => m('.tabs', [
|
|
42
|
-
['routes', 'Routes'],
|
|
43
|
-
['
|
|
51
|
+
['routes', 'Routes (' + state.routes.length + ')'],
|
|
52
|
+
['models', 'Models (' + state.models.length + ')'],
|
|
53
|
+
['services', 'Services (' + state.services.length + ')'],
|
|
54
|
+
['plugins', 'Plugins (' + state.plugins.length + ')'],
|
|
44
55
|
['config', 'Config']
|
|
45
56
|
].map(([id, label]) =>
|
|
46
57
|
m('button.tab', {
|
|
@@ -72,32 +83,36 @@ const FilterBar = {
|
|
|
72
83
|
};
|
|
73
84
|
|
|
74
85
|
const MethodBadge = {
|
|
75
|
-
view: (vnode) => m('span.method-badge.method-' + vnode.attrs.method,
|
|
76
|
-
vnode.attrs.method.toUpperCase()
|
|
86
|
+
view: (vnode) => m('span.method-badge.method-' + (vnode.attrs.method || 'get').toLowerCase(),
|
|
87
|
+
(vnode.attrs.method || 'GET').toUpperCase()
|
|
77
88
|
)
|
|
78
89
|
};
|
|
79
90
|
|
|
80
91
|
const TypeBadge = {
|
|
81
|
-
view: (vnode) =>
|
|
82
|
-
vnode.attrs.type
|
|
83
|
-
|
|
92
|
+
view: (vnode) => {
|
|
93
|
+
const rawType = vnode.attrs.type || 'page';
|
|
94
|
+
const label = rawType === 'page' ? 'SSR' : rawType.toUpperCase();
|
|
95
|
+
return m('span.type-badge.type-' + (rawType === 'page' ? 'ssr' : rawType), label);
|
|
96
|
+
}
|
|
84
97
|
};
|
|
85
98
|
|
|
86
99
|
const RoutesTable = {
|
|
87
100
|
view: () => {
|
|
88
101
|
let routes = state.routes;
|
|
89
102
|
|
|
90
|
-
// Apply filter
|
|
91
|
-
if (state.filter
|
|
92
|
-
routes = routes.filter(r => r.type ===
|
|
103
|
+
// Apply filter: SSR matches both 'ssr' and 'page'
|
|
104
|
+
if (state.filter === 'ssr') {
|
|
105
|
+
routes = routes.filter(r => r.type === 'ssr' || r.type === 'page');
|
|
106
|
+
} else if (state.filter === 'api') {
|
|
107
|
+
routes = routes.filter(r => r.type === 'api');
|
|
93
108
|
}
|
|
94
109
|
|
|
95
|
-
// Apply search
|
|
110
|
+
// Apply search: checks path or pattern or file
|
|
96
111
|
if (state.search) {
|
|
97
112
|
const search = state.search.toLowerCase();
|
|
98
113
|
routes = routes.filter(r =>
|
|
99
|
-
r.pattern.toLowerCase().includes(search) ||
|
|
100
|
-
r.file.toLowerCase().includes(search)
|
|
114
|
+
((r.path || r.pattern || '')).toLowerCase().includes(search) ||
|
|
115
|
+
((r.file || '')).toLowerCase().includes(search)
|
|
101
116
|
);
|
|
102
117
|
}
|
|
103
118
|
|
|
@@ -121,17 +136,18 @@ const RoutesTable = {
|
|
|
121
136
|
])
|
|
122
137
|
),
|
|
123
138
|
m('tbody',
|
|
124
|
-
routes.map(route =>
|
|
125
|
-
|
|
139
|
+
routes.map(route => {
|
|
140
|
+
const displayPath = route.path || route.pattern || '/';
|
|
141
|
+
return m('tr', [
|
|
126
142
|
m('td', m(MethodBadge, { method: route.method })),
|
|
127
143
|
m('td', [
|
|
128
|
-
m('span.code',
|
|
144
|
+
m('span.code', displayPath),
|
|
129
145
|
route.isDynamic ? m('span.dynamic-indicator', '⚡ dynamic') : null
|
|
130
146
|
]),
|
|
131
|
-
m('td', m('span.file-path', route.file)),
|
|
147
|
+
m('td', m('span.file-path', route.file || 'custom')),
|
|
132
148
|
m('td', m(TypeBadge, { type: route.type }))
|
|
133
|
-
])
|
|
134
|
-
)
|
|
149
|
+
]);
|
|
150
|
+
})
|
|
135
151
|
)
|
|
136
152
|
])
|
|
137
153
|
);
|
|
@@ -140,7 +156,7 @@ const RoutesTable = {
|
|
|
140
156
|
|
|
141
157
|
const StatsGrid = {
|
|
142
158
|
view: () => {
|
|
143
|
-
const ssrCount = state.routes.filter(r => r.type === 'ssr').length;
|
|
159
|
+
const ssrCount = state.routes.filter(r => r.type === 'ssr' || r.type === 'page').length;
|
|
144
160
|
const apiCount = state.routes.filter(r => r.type === 'api').length;
|
|
145
161
|
const dynamicCount = state.routes.filter(r => r.isDynamic).length;
|
|
146
162
|
|
|
@@ -173,6 +189,116 @@ const RoutesView = {
|
|
|
173
189
|
])
|
|
174
190
|
};
|
|
175
191
|
|
|
192
|
+
const ModelsView = {
|
|
193
|
+
view: () => {
|
|
194
|
+
const models = state.models || [];
|
|
195
|
+
if (models.length === 0) {
|
|
196
|
+
return m('.table-container',
|
|
197
|
+
m('.empty-state', [
|
|
198
|
+
Icons.empty(),
|
|
199
|
+
m('p', 'No ORM models registered in core/orm')
|
|
200
|
+
])
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return m('div', [
|
|
205
|
+
m('.stats-grid', [
|
|
206
|
+
m('.card', [
|
|
207
|
+
m('.card-title', 'Total Models'),
|
|
208
|
+
m('.card-value', models.length)
|
|
209
|
+
]),
|
|
210
|
+
m('.card', [
|
|
211
|
+
m('.card-title', 'Cached Models'),
|
|
212
|
+
m('.card-value', models.filter(m => Boolean(m.cache)).length)
|
|
213
|
+
]),
|
|
214
|
+
m('.card', [
|
|
215
|
+
m('.card-title', 'Soft-Delete Enabled'),
|
|
216
|
+
m('.card-value', models.filter(m => Boolean(m.softDelete)).length)
|
|
217
|
+
])
|
|
218
|
+
]),
|
|
219
|
+
m('.table-container',
|
|
220
|
+
m('table', [
|
|
221
|
+
m('thead',
|
|
222
|
+
m('tr', [
|
|
223
|
+
m('th', 'Model'),
|
|
224
|
+
m('th', 'Table'),
|
|
225
|
+
m('th', 'Primary Key'),
|
|
226
|
+
m('th', 'Columns'),
|
|
227
|
+
m('th', 'Soft Delete'),
|
|
228
|
+
m('th', 'Cache')
|
|
229
|
+
])
|
|
230
|
+
),
|
|
231
|
+
m('tbody',
|
|
232
|
+
models.map(mod =>
|
|
233
|
+
m('tr', [
|
|
234
|
+
m('td', m('strong', mod.name)),
|
|
235
|
+
m('td', m('span.code-badge', mod.table)),
|
|
236
|
+
m('td', m('span.badge.badge-blue', mod.primaryKey || 'id')),
|
|
237
|
+
m('td', (mod.columnCount || 0) + ' columns'),
|
|
238
|
+
m('td', mod.softDelete ? m('span.badge.badge-green', 'Enabled') : m('span.badge.badge-gray', 'Disabled')),
|
|
239
|
+
m('td', mod.cache ? m('span.badge.badge-purple', String(mod.cache)) : m('span.badge.badge-gray', 'None'))
|
|
240
|
+
])
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
])
|
|
244
|
+
)
|
|
245
|
+
]);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const ServicesView = {
|
|
250
|
+
view: () => {
|
|
251
|
+
const services = state.services || [];
|
|
252
|
+
if (services.length === 0) {
|
|
253
|
+
return m('.table-container',
|
|
254
|
+
m('.empty-state', [
|
|
255
|
+
Icons.empty(),
|
|
256
|
+
m('p', 'No services registered in services/ or ServiceRegistry')
|
|
257
|
+
])
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return m('div', [
|
|
262
|
+
m('.stats-grid', [
|
|
263
|
+
m('.card', [
|
|
264
|
+
m('.card-title', 'Total Services'),
|
|
265
|
+
m('.card-value', services.length)
|
|
266
|
+
]),
|
|
267
|
+
m('.card', [
|
|
268
|
+
m('.card-title', 'ACID Transactions'),
|
|
269
|
+
m('.card-value', services.filter(s => s.transaction).length)
|
|
270
|
+
]),
|
|
271
|
+
m('.card', [
|
|
272
|
+
m('.card-title', 'Auth Protected'),
|
|
273
|
+
m('.card-value', services.filter(s => s.auth).length)
|
|
274
|
+
])
|
|
275
|
+
]),
|
|
276
|
+
m('.table-container',
|
|
277
|
+
m('table', [
|
|
278
|
+
m('thead',
|
|
279
|
+
m('tr', [
|
|
280
|
+
m('th', 'Service Name'),
|
|
281
|
+
m('th', 'Transaction (ACID)'),
|
|
282
|
+
m('th', 'Auth Guard'),
|
|
283
|
+
m('th', 'Schema Validation')
|
|
284
|
+
])
|
|
285
|
+
),
|
|
286
|
+
m('tbody',
|
|
287
|
+
services.map(srv =>
|
|
288
|
+
m('tr', [
|
|
289
|
+
m('td', m('span.code-badge', srv.name)),
|
|
290
|
+
m('td', srv.transaction ? m('span.badge.badge-green', 'Active') : m('span.badge.badge-gray', 'No')),
|
|
291
|
+
m('td', srv.auth ? m('span.badge.badge-orange', String(srv.auth)) : m('span.badge.badge-gray', 'Public')),
|
|
292
|
+
m('td', srv.hasSchema ? m('span.badge.badge-blue', 'Zod Schema') : m('span.badge.badge-gray', 'None'))
|
|
293
|
+
])
|
|
294
|
+
)
|
|
295
|
+
)
|
|
296
|
+
])
|
|
297
|
+
)
|
|
298
|
+
]);
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
176
302
|
const PluginsView = {
|
|
177
303
|
view: () => {
|
|
178
304
|
if (state.plugins.length === 0) {
|
|
@@ -254,6 +380,8 @@ const App = {
|
|
|
254
380
|
m(Header),
|
|
255
381
|
m(Tabs),
|
|
256
382
|
state.activeTab === 'routes' ? m(RoutesView) : null,
|
|
383
|
+
state.activeTab === 'models' ? m(ModelsView) : null,
|
|
384
|
+
state.activeTab === 'services' ? m(ServicesView) : null,
|
|
257
385
|
state.activeTab === 'plugins' ? m(PluginsView) : null,
|
|
258
386
|
state.activeTab === 'config' ? m(ConfigView) : null
|
|
259
387
|
])
|
|
@@ -262,6 +390,3 @@ const App = {
|
|
|
262
390
|
// Mount app
|
|
263
391
|
m.mount(document.getElementById('app'), App);
|
|
264
392
|
`;
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|