webspresso 0.0.97 → 0.0.99
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 +163 -5
- package/.agents/AGENTS.md +1 -1
- package/.agents/ARCHITECTURE.md +10 -0
- package/.agents/AUTH.md +169 -0
- package/.agents/CLI.md +195 -1
- package/.agents/ORM.md +88 -4
- package/.agents/PLUGINS.md +221 -9
- package/.agents/QUEUE.md +127 -0
- package/.agents/ROUTING.md +127 -7
- package/AGENTS.md +1 -0
- package/bin/commands/add-esbuild.js +80 -0
- package/bin/commands/add-tailwind.js +76 -31
- package/bin/commands/analytics-prune.js +57 -0
- package/bin/commands/api.js +207 -15
- 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/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/page.js +6 -3
- package/bin/commands/queue-prune.js +59 -0
- package/bin/commands/routes.js +52 -0
- 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 +21 -12
- package/core/auth/index.js +60 -0
- package/core/auth/manager.js +134 -0
- package/core/auth/middleware.js +88 -0
- package/core/auth/oauth.js +517 -0
- package/core/auth/qrcode.js +499 -0
- package/core/auth/totp.js +504 -0
- 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/eager-loader.js +144 -10
- package/core/orm/index.js +20 -7
- package/core/orm/model.js +53 -7
- package/core/orm/query-builder.js +90 -4
- package/core/orm/repository.js +44 -3
- package/core/orm/search.js +213 -0
- 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/cron.js +331 -0
- package/core/queue/index.js +8 -1
- package/core/queue/manager.js +205 -1
- package/core/queue/scheduler.js +359 -0
- package/core/sse/index.js +19 -0
- package/core/sse/stream.js +430 -0
- package/core/ssr/stream.js +11 -0
- package/index.d.ts +1188 -10
- package/index.js +88 -96
- package/package.json +1 -1
- package/plugins/admin-panel/api.js +279 -17
- package/plugins/admin-panel/app.js +114 -22
- 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 +333 -36
- 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 +248 -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/client/verify-spa-parts.js +2 -0
- package/plugins/admin-panel/core/admin-module.js +13 -7
- package/plugins/admin-panel/core/api-extensions.js +30 -2
- package/plugins/admin-panel/core/registry.js +96 -16
- package/plugins/admin-panel/field-renderers/file-upload.js +187 -104
- package/plugins/admin-panel/field-renderers/index.js +2 -0
- package/plugins/admin-panel/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/api-key/crypto.js +101 -0
- package/plugins/api-key/index.js +288 -0
- package/plugins/api-key/middleware.js +461 -0
- package/plugins/api-key/migration-template.js +48 -0
- package/plugins/api-key/model.js +50 -0
- package/plugins/api-key/scopes.js +125 -0
- package/plugins/audit-log/admin-component.js +487 -59
- package/plugins/audit-log/api-handlers.js +148 -0
- package/plugins/audit-log/index.js +153 -3
- package/plugins/audit-log/purge.js +60 -10
- 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/admin-component.js +20 -4
- package/plugins/file-manager/index.js +21 -4
- package/plugins/index.js +10 -0
- package/plugins/oauth/index.js +338 -0
- package/plugins/orm-cache-admin/index.js +4 -2
- package/plugins/queue/index.js +15 -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 +23 -0
- 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 +4 -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/discovery/file-route-parser.js +44 -1
- package/src/discovery/scan-routes.js +2 -2
- package/src/file-router.js +347 -162
- package/src/helpers.js +967 -98
- package/src/njk-frontmatter.js +12 -0
- package/src/pages/page-loader.js +61 -11
- package/src/routing/cascading.js +378 -0
- package/src/routing/middleware-resolver.js +92 -0
- package/src/server.js +635 -63
|
@@ -9,6 +9,50 @@ module.exports = components + `
|
|
|
9
9
|
// Set route prefix to admin path
|
|
10
10
|
m.route.prefix = window.__ADMIN_PATH__ || '/_admin';
|
|
11
11
|
|
|
12
|
+
// Title formatting and synchronization helper
|
|
13
|
+
function formatAdminTitle(pageTitle, baseTitle, template) {
|
|
14
|
+
baseTitle = baseTitle || 'Admin Panel';
|
|
15
|
+
if (!pageTitle || pageTitle === baseTitle) {
|
|
16
|
+
return baseTitle;
|
|
17
|
+
}
|
|
18
|
+
if (typeof template === 'function') {
|
|
19
|
+
return template(pageTitle, baseTitle);
|
|
20
|
+
}
|
|
21
|
+
if (typeof template === 'string' && template.trim()) {
|
|
22
|
+
var formatted = template;
|
|
23
|
+
if (formatted.indexOf('%s') !== -1) {
|
|
24
|
+
formatted = formatted.replace(/%s/g, pageTitle);
|
|
25
|
+
}
|
|
26
|
+
if (formatted.indexOf('%b') !== -1) {
|
|
27
|
+
formatted = formatted.replace(/%b/g, baseTitle);
|
|
28
|
+
}
|
|
29
|
+
return formatted;
|
|
30
|
+
}
|
|
31
|
+
return pageTitle + ' | ' + baseTitle;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function setAdminPageTitle(pageTitle, options) {
|
|
35
|
+
try {
|
|
36
|
+
var config = window.__ADMIN_CONFIG__ || {};
|
|
37
|
+
var settings = config.settings || {};
|
|
38
|
+
var baseTitle = settings.title || 'Admin Panel';
|
|
39
|
+
var template = (options && options.template) || settings.titleTemplate || '%s | %b';
|
|
40
|
+
|
|
41
|
+
if (options && options.raw) {
|
|
42
|
+
document.title = pageTitle || baseTitle;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
document.title = formatAdminTitle(pageTitle, baseTitle, template);
|
|
47
|
+
} catch (e) {
|
|
48
|
+
if (pageTitle) {
|
|
49
|
+
document.title = pageTitle;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
window.formatAdminTitle = formatAdminTitle;
|
|
54
|
+
window.setAdminPageTitle = setAdminPageTitle;
|
|
55
|
+
|
|
12
56
|
// Helper to check auth status
|
|
13
57
|
async function checkAuth() {
|
|
14
58
|
if (state.user) return true;
|
|
@@ -45,7 +89,7 @@ function getUserManagementModel() {
|
|
|
45
89
|
async function guardUserManagementRoutes() {
|
|
46
90
|
var isAuth = await checkAuth();
|
|
47
91
|
if (!isAuth) {
|
|
48
|
-
redirectToLogin();
|
|
92
|
+
redirectToLogin('expired');
|
|
49
93
|
return false;
|
|
50
94
|
}
|
|
51
95
|
if (!getUserManagementModel()) {
|
|
@@ -98,13 +142,20 @@ const UserSessionsPage = {
|
|
|
98
142
|
m('td.px-3.py-2.text-gray-700.dark:text-slate-300', r.created_at ? new Date(r.created_at).toLocaleString() : ''),
|
|
99
143
|
m('td.px-3.py-2', m('button.text-red-600.dark:text-red-400.text-xs', {
|
|
100
144
|
onclick: async function () {
|
|
101
|
-
|
|
145
|
+
var ok = await confirmAction({
|
|
146
|
+
title: 'Revoke Session',
|
|
147
|
+
message: 'Revoke this session?',
|
|
148
|
+
danger: true,
|
|
149
|
+
confirmText: 'Revoke'
|
|
150
|
+
});
|
|
151
|
+
if (!ok) return;
|
|
102
152
|
try {
|
|
103
153
|
await api.delete('/users/sessions/' + encodeURIComponent(r.token));
|
|
104
154
|
vnode.state.rows = vnode.state.rows.filter(function (x) { return x.token !== r.token; });
|
|
155
|
+
notifySuccess('Session revoked successfully');
|
|
105
156
|
m.redraw();
|
|
106
157
|
} catch (err) {
|
|
107
|
-
|
|
158
|
+
notifyError(err.message || String(err));
|
|
108
159
|
}
|
|
109
160
|
},
|
|
110
161
|
}, 'Revoke')),
|
|
@@ -122,12 +173,15 @@ var routes = {
|
|
|
122
173
|
onmatch: async () => {
|
|
123
174
|
const needsSetup = await checkSetupNeeded();
|
|
124
175
|
if (needsSetup) {
|
|
176
|
+
setAdminPageTitle('Setup');
|
|
125
177
|
return SetupForm;
|
|
126
178
|
}
|
|
127
179
|
const isAuth = await checkAuth();
|
|
128
180
|
if (!isAuth) {
|
|
181
|
+
setAdminPageTitle('Sign In');
|
|
129
182
|
return LoginForm;
|
|
130
183
|
}
|
|
184
|
+
setAdminPageTitle('Dashboard');
|
|
131
185
|
return Dashboard;
|
|
132
186
|
}
|
|
133
187
|
},
|
|
@@ -138,6 +192,7 @@ var routes = {
|
|
|
138
192
|
m.route.set(consumeIntendedRoute('/'));
|
|
139
193
|
return;
|
|
140
194
|
}
|
|
195
|
+
setAdminPageTitle('Sign In');
|
|
141
196
|
return LoginForm;
|
|
142
197
|
}
|
|
143
198
|
},
|
|
@@ -148,6 +203,7 @@ var routes = {
|
|
|
148
203
|
m.route.set('/login');
|
|
149
204
|
return;
|
|
150
205
|
}
|
|
206
|
+
setAdminPageTitle('Setup');
|
|
151
207
|
return SetupForm;
|
|
152
208
|
}
|
|
153
209
|
},
|
|
@@ -155,9 +211,10 @@ var routes = {
|
|
|
155
211
|
onmatch: async () => {
|
|
156
212
|
const isAuth = await checkAuth();
|
|
157
213
|
if (!isAuth) {
|
|
158
|
-
redirectToLogin();
|
|
214
|
+
redirectToLogin('expired');
|
|
159
215
|
return;
|
|
160
216
|
}
|
|
217
|
+
setAdminPageTitle('Settings');
|
|
161
218
|
return SettingsPage;
|
|
162
219
|
}
|
|
163
220
|
},
|
|
@@ -165,9 +222,10 @@ var routes = {
|
|
|
165
222
|
onmatch: async () => {
|
|
166
223
|
const isAuth = await checkAuth();
|
|
167
224
|
if (!isAuth) {
|
|
168
|
-
redirectToLogin();
|
|
225
|
+
redirectToLogin('expired');
|
|
169
226
|
return;
|
|
170
227
|
}
|
|
228
|
+
setAdminPageTitle('Profile');
|
|
171
229
|
return ProfilePage;
|
|
172
230
|
}
|
|
173
231
|
},
|
|
@@ -175,15 +233,17 @@ var routes = {
|
|
|
175
233
|
onmatch: async () => {
|
|
176
234
|
const isAuth = await checkAuth();
|
|
177
235
|
if (!isAuth) {
|
|
178
|
-
redirectToLogin();
|
|
236
|
+
redirectToLogin('expired');
|
|
179
237
|
return;
|
|
180
238
|
}
|
|
239
|
+
setAdminPageTitle('Staff Users');
|
|
181
240
|
return AdminUsersPage;
|
|
182
241
|
}
|
|
183
242
|
},
|
|
184
243
|
'/users/sessions': {
|
|
185
244
|
onmatch: async () => {
|
|
186
245
|
if (!(await guardUserManagementRoutes())) return;
|
|
246
|
+
setAdminPageTitle('Active Sessions');
|
|
187
247
|
return UserSessionsPage;
|
|
188
248
|
}
|
|
189
249
|
},
|
|
@@ -213,9 +273,11 @@ var routes = {
|
|
|
213
273
|
onmatch: async () => {
|
|
214
274
|
const isAuth = await checkAuth();
|
|
215
275
|
if (!isAuth) {
|
|
216
|
-
redirectToLogin();
|
|
276
|
+
redirectToLogin('expired');
|
|
217
277
|
return;
|
|
218
278
|
}
|
|
279
|
+
const model = m.route.param('model');
|
|
280
|
+
setAdminPageTitle(model);
|
|
219
281
|
return RecordList;
|
|
220
282
|
}
|
|
221
283
|
},
|
|
@@ -223,9 +285,11 @@ var routes = {
|
|
|
223
285
|
onmatch: async () => {
|
|
224
286
|
const isAuth = await checkAuth();
|
|
225
287
|
if (!isAuth) {
|
|
226
|
-
redirectToLogin();
|
|
288
|
+
redirectToLogin('expired');
|
|
227
289
|
return;
|
|
228
290
|
}
|
|
291
|
+
const model = m.route.param('model');
|
|
292
|
+
setAdminPageTitle('New ' + model);
|
|
229
293
|
return RecordForm;
|
|
230
294
|
}
|
|
231
295
|
},
|
|
@@ -233,9 +297,11 @@ var routes = {
|
|
|
233
297
|
onmatch: async () => {
|
|
234
298
|
const isAuth = await checkAuth();
|
|
235
299
|
if (!isAuth) {
|
|
236
|
-
redirectToLogin();
|
|
300
|
+
redirectToLogin('expired');
|
|
237
301
|
return;
|
|
238
302
|
}
|
|
303
|
+
const model = m.route.param('model');
|
|
304
|
+
setAdminPageTitle('Edit ' + model);
|
|
239
305
|
return RecordForm;
|
|
240
306
|
}
|
|
241
307
|
},
|
|
@@ -243,10 +309,16 @@ var routes = {
|
|
|
243
309
|
|
|
244
310
|
function wrapCustomPageWithLayout(page, rawComp) {
|
|
245
311
|
if (!rawComp) return createCustomPage(page);
|
|
246
|
-
if (page.layout === false) return rawComp;
|
|
247
312
|
|
|
248
313
|
return {
|
|
249
314
|
oninit: function(vnode) {
|
|
315
|
+
if (page) {
|
|
316
|
+
if (page.documentTitle) {
|
|
317
|
+
document.title = page.documentTitle;
|
|
318
|
+
} else if (page.title || page.id) {
|
|
319
|
+
setAdminPageTitle(page.title || page.id);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
250
322
|
try {
|
|
251
323
|
const instance = typeof rawComp === 'function' ? rawComp.call(rawComp, vnode) : rawComp;
|
|
252
324
|
vnode.state.instance = instance;
|
|
@@ -293,14 +365,15 @@ function wrapCustomPageWithLayout(page, rawComp) {
|
|
|
293
365
|
view: function(vnode) {
|
|
294
366
|
if (vnode.state.renderError) {
|
|
295
367
|
var err = vnode.state.renderError;
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
m(
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
368
|
+
return typeof renderCustomPageError === 'function'
|
|
369
|
+
? renderCustomPageError(page, err)
|
|
370
|
+
: m(Layout, [
|
|
371
|
+
m(Breadcrumb, { items: [{ label: page.title, href: page.path }] }),
|
|
372
|
+
m('.p-6.bg-red-50.border.border-red-200.rounded-xl', [
|
|
373
|
+
m('h3.text-lg.font-semibold.text-red-800', 'Custom Page Component Error'),
|
|
374
|
+
m('p.text-sm.text-red-600.mt-1', err.message || String(err)),
|
|
375
|
+
])
|
|
376
|
+
]);
|
|
304
377
|
}
|
|
305
378
|
const comp = vnode.state.instance || rawComp;
|
|
306
379
|
var content;
|
|
@@ -314,11 +387,25 @@ function wrapCustomPageWithLayout(page, rawComp) {
|
|
|
314
387
|
}
|
|
315
388
|
} catch (err) {
|
|
316
389
|
console.error('[Admin Panel] Error rendering custom page "' + page.id + '":', err);
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
m(
|
|
390
|
+
return typeof renderCustomPageError === 'function'
|
|
391
|
+
? renderCustomPageError(page, err)
|
|
392
|
+
: m(Layout, [
|
|
393
|
+
m(Breadcrumb, { items: [{ label: page.title, href: page.path }] }),
|
|
394
|
+
m('.p-6.bg-red-50.border.border-red-200.rounded-xl', [
|
|
395
|
+
m('h3.text-lg.font-semibold.text-red-800', 'Custom Page Component Error'),
|
|
396
|
+
m('p.text-sm.text-red-600.mt-1', err.message || String(err)),
|
|
397
|
+
])
|
|
398
|
+
]);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (page.layout === false) {
|
|
402
|
+
return m('div.custom-canvas-page', [
|
|
403
|
+
content,
|
|
404
|
+
typeof ToastContainer !== 'undefined' ? m(ToastContainer) : null,
|
|
405
|
+
typeof ConfirmModalContainer !== 'undefined' ? m(ConfirmModalContainer) : null,
|
|
320
406
|
]);
|
|
321
407
|
}
|
|
408
|
+
|
|
322
409
|
if (content && (content.tag === Layout || (content.tag && content.tag.name === 'Layout'))) {
|
|
323
410
|
return content;
|
|
324
411
|
}
|
|
@@ -340,9 +427,14 @@ if (config && config.pages) {
|
|
|
340
427
|
onmatch: async () => {
|
|
341
428
|
const isAuth = await checkAuth();
|
|
342
429
|
if (!isAuth) {
|
|
343
|
-
redirectToLogin();
|
|
430
|
+
redirectToLogin('expired');
|
|
344
431
|
return;
|
|
345
432
|
}
|
|
433
|
+
if (page.documentTitle) {
|
|
434
|
+
document.title = page.documentTitle;
|
|
435
|
+
} else {
|
|
436
|
+
setAdminPageTitle(page.title || page.id);
|
|
437
|
+
}
|
|
346
438
|
const comp = (window.__customPages && window.__customPages[page.id])
|
|
347
439
|
? wrapCustomPageWithLayout(page, window.__customPages[page.id])
|
|
348
440
|
: createCustomPage(page);
|
|
@@ -16,11 +16,12 @@ const api = {
|
|
|
16
16
|
if (!response.ok) {
|
|
17
17
|
const error = await response.json().catch(() => ({ error: 'Request failed' }));
|
|
18
18
|
if (response.status === 401 && path.indexOf('/auth/') !== 0) {
|
|
19
|
-
redirectToLogin();
|
|
19
|
+
redirectToLogin('expired');
|
|
20
20
|
}
|
|
21
|
-
const err = new Error(error.error || 'Request failed');
|
|
21
|
+
const err = new Error(error.error || error.message || 'Request failed');
|
|
22
22
|
if (error.fields) err.fields = error.fields;
|
|
23
23
|
err.response = error;
|
|
24
|
+
err.status = response.status;
|
|
24
25
|
throw err;
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -90,6 +91,8 @@ const state = {
|
|
|
90
91
|
needsSetup: false,
|
|
91
92
|
loading: false,
|
|
92
93
|
error: null,
|
|
94
|
+
notice: null,
|
|
95
|
+
toasts: [],
|
|
93
96
|
models: [],
|
|
94
97
|
currentModel: null,
|
|
95
98
|
currentModelMeta: null, // Full model metadata with columns
|
|
@@ -118,14 +121,19 @@ var INTENDED_ROUTE_KEY = 'webspresso.admin.intendedRoute';
|
|
|
118
121
|
|
|
119
122
|
function isSafeIntendedRoute(path) {
|
|
120
123
|
if (!path || typeof path !== 'string') return false;
|
|
121
|
-
|
|
122
|
-
if (
|
|
124
|
+
var p = path.trim();
|
|
125
|
+
if (p.charAt(0) !== '/' || p.indexOf('//') === 0 || p.indexOf('/\\') === 0) return false;
|
|
126
|
+
if (p === '/login' || p === '/setup' || p.indexOf('/login?') === 0 || p.indexOf('/setup?') === 0) return false;
|
|
127
|
+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(p)) return false;
|
|
123
128
|
return true;
|
|
124
129
|
}
|
|
125
130
|
|
|
126
|
-
function saveIntendedRoute() {
|
|
131
|
+
function saveIntendedRoute(explicitPath) {
|
|
127
132
|
try {
|
|
128
|
-
var path =
|
|
133
|
+
var path = explicitPath;
|
|
134
|
+
if (!path && typeof m !== 'undefined' && m.route && typeof m.route.get === 'function') {
|
|
135
|
+
path = m.route.get();
|
|
136
|
+
}
|
|
129
137
|
if (!isSafeIntendedRoute(path)) return;
|
|
130
138
|
sessionStorage.setItem(INTENDED_ROUTE_KEY, path);
|
|
131
139
|
} catch (e) {}
|
|
@@ -139,17 +147,314 @@ function clearIntendedRoute() {
|
|
|
139
147
|
|
|
140
148
|
function consumeIntendedRoute(defaultPath) {
|
|
141
149
|
defaultPath = defaultPath || '/';
|
|
150
|
+
|
|
151
|
+
// 1. Try route parameter or query string returnTo
|
|
152
|
+
try {
|
|
153
|
+
if (typeof m !== 'undefined' && m.route && typeof m.route.param === 'function') {
|
|
154
|
+
var param = m.route.param('returnTo');
|
|
155
|
+
if (param && isSafeIntendedRoute(param)) {
|
|
156
|
+
clearIntendedRoute();
|
|
157
|
+
return param;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
} catch (e) {}
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
if (typeof window !== 'undefined' && window.location && window.location.search) {
|
|
164
|
+
var match = window.location.search.match(/[?&]returnTo=([^&]+)/);
|
|
165
|
+
if (match && match[1]) {
|
|
166
|
+
var decoded = decodeURIComponent(match[1]);
|
|
167
|
+
if (isSafeIntendedRoute(decoded)) {
|
|
168
|
+
clearIntendedRoute();
|
|
169
|
+
return decoded;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} catch (e) {}
|
|
174
|
+
|
|
175
|
+
// 2. Fall back to sessionStorage
|
|
142
176
|
try {
|
|
143
177
|
var path = sessionStorage.getItem(INTENDED_ROUTE_KEY);
|
|
144
178
|
sessionStorage.removeItem(INTENDED_ROUTE_KEY);
|
|
145
179
|
if (isSafeIntendedRoute(path)) return path;
|
|
146
180
|
} catch (e) {}
|
|
181
|
+
|
|
147
182
|
return defaultPath;
|
|
148
183
|
}
|
|
149
184
|
|
|
150
|
-
function redirectToLogin() {
|
|
151
|
-
|
|
152
|
-
|
|
185
|
+
function redirectToLogin(reason) {
|
|
186
|
+
var currentRoute = '';
|
|
187
|
+
try {
|
|
188
|
+
if (typeof m !== 'undefined' && m.route && typeof m.route.get === 'function') {
|
|
189
|
+
currentRoute = m.route.get();
|
|
190
|
+
}
|
|
191
|
+
} catch (e) {}
|
|
192
|
+
|
|
193
|
+
if (isSafeIntendedRoute(currentRoute)) {
|
|
194
|
+
saveIntendedRoute(currentRoute);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
var params = [];
|
|
198
|
+
if (isSafeIntendedRoute(currentRoute)) {
|
|
199
|
+
params.push('returnTo=' + encodeURIComponent(currentRoute));
|
|
200
|
+
}
|
|
201
|
+
if (reason) {
|
|
202
|
+
params.push('reason=' + encodeURIComponent(reason));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
var target = '/login' + (params.length > 0 ? '?' + params.join('&') : '');
|
|
206
|
+
try {
|
|
207
|
+
if (typeof m !== 'undefined' && m.route && typeof m.route.set === 'function') {
|
|
208
|
+
m.route.set(target);
|
|
209
|
+
}
|
|
210
|
+
} catch (e) {}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ==========================================
|
|
214
|
+
// Toast Notification & Confirm Modal System
|
|
215
|
+
// ==========================================
|
|
216
|
+
var toastState = (typeof window !== 'undefined' && window.toastState) ? window.toastState : {
|
|
217
|
+
items: [],
|
|
218
|
+
add: function(type, message, options) {
|
|
219
|
+
options = options || {};
|
|
220
|
+
if (typeof type === 'object') {
|
|
221
|
+
options = type;
|
|
222
|
+
type = options.type || 'info';
|
|
223
|
+
message = options.message || '';
|
|
224
|
+
}
|
|
225
|
+
var id = 'toast_' + Math.random().toString(36).substring(2, 9);
|
|
226
|
+
var duration = options.duration !== undefined ? options.duration : 4000;
|
|
227
|
+
var item = {
|
|
228
|
+
id: id,
|
|
229
|
+
type: type || 'info',
|
|
230
|
+
message: message,
|
|
231
|
+
title: options.title || '',
|
|
232
|
+
duration: duration,
|
|
233
|
+
};
|
|
234
|
+
toastState.items.push(item);
|
|
235
|
+
if (typeof state !== 'undefined') {
|
|
236
|
+
state.toasts = toastState.items;
|
|
237
|
+
}
|
|
238
|
+
if (item.duration > 0) {
|
|
239
|
+
setTimeout(function() {
|
|
240
|
+
toastState.remove(id);
|
|
241
|
+
}, item.duration);
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
if (typeof m !== 'undefined' && typeof m.redraw === 'function') {
|
|
245
|
+
m.redraw();
|
|
246
|
+
}
|
|
247
|
+
} catch (e) {}
|
|
248
|
+
return id;
|
|
249
|
+
},
|
|
250
|
+
remove: function(id) {
|
|
251
|
+
toastState.items = toastState.items.filter(function(t) { return t.id !== id; });
|
|
252
|
+
if (typeof state !== 'undefined') {
|
|
253
|
+
state.toasts = toastState.items;
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
if (typeof m !== 'undefined' && typeof m.redraw === 'function') {
|
|
257
|
+
m.redraw();
|
|
258
|
+
}
|
|
259
|
+
} catch (e) {}
|
|
260
|
+
},
|
|
261
|
+
clear: function() {
|
|
262
|
+
toastState.items = [];
|
|
263
|
+
if (typeof state !== 'undefined') {
|
|
264
|
+
state.toasts = [];
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
if (typeof m !== 'undefined' && typeof m.redraw === 'function') {
|
|
268
|
+
m.redraw();
|
|
269
|
+
}
|
|
270
|
+
} catch (e) {}
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
function normalizeToastOptions(options) {
|
|
275
|
+
if (typeof options === 'number') {
|
|
276
|
+
return { duration: options };
|
|
277
|
+
}
|
|
278
|
+
return options || {};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
var notify = function(message, type, options) {
|
|
282
|
+
var opts = normalizeToastOptions(options);
|
|
283
|
+
if (typeof type === 'object') {
|
|
284
|
+
opts = type;
|
|
285
|
+
type = opts.type || 'info';
|
|
286
|
+
}
|
|
287
|
+
return toastState.add(type || 'info', message, opts);
|
|
288
|
+
};
|
|
289
|
+
notify.success = function(message, options) { return toastState.add('success', message, normalizeToastOptions(options)); };
|
|
290
|
+
notify.error = function(message, options) { return toastState.add('error', message, normalizeToastOptions(options)); };
|
|
291
|
+
notify.warning = function(message, options) { return toastState.add('warning', message, normalizeToastOptions(options)); };
|
|
292
|
+
notify.info = function(message, options) { return toastState.add('info', message, normalizeToastOptions(options)); };
|
|
293
|
+
notify.remove = function(id) { return toastState.remove(id); };
|
|
294
|
+
notify.clear = function() { return toastState.clear(); };
|
|
295
|
+
|
|
296
|
+
var ToastContainer = {
|
|
297
|
+
view: function() {
|
|
298
|
+
var items = (typeof toastState !== 'undefined' && toastState.items) || (typeof state !== 'undefined' && state.toasts) || [];
|
|
299
|
+
if (!items || items.length === 0) return null;
|
|
300
|
+
return m('div.fixed.top-4.right-4.z-50.flex.flex-col.gap-2.5.max-w-sm.w-full.pointer-events-none.px-4.sm:px-0',
|
|
301
|
+
items.map(function(t) {
|
|
302
|
+
var typeConfig = {
|
|
303
|
+
success: {
|
|
304
|
+
border: 'border-emerald-200 dark:border-emerald-800/80',
|
|
305
|
+
bg: 'bg-white dark:bg-slate-900',
|
|
306
|
+
iconColor: 'text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-950/60',
|
|
307
|
+
symbol: '✓',
|
|
308
|
+
},
|
|
309
|
+
error: {
|
|
310
|
+
border: 'border-red-200 dark:border-red-800/80',
|
|
311
|
+
bg: 'bg-white dark:bg-slate-900',
|
|
312
|
+
iconColor: 'text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-950/60',
|
|
313
|
+
symbol: '!',
|
|
314
|
+
},
|
|
315
|
+
warning: {
|
|
316
|
+
border: 'border-amber-200 dark:border-amber-800/80',
|
|
317
|
+
bg: 'bg-white dark:bg-slate-900',
|
|
318
|
+
iconColor: 'text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-950/60',
|
|
319
|
+
symbol: '⚠',
|
|
320
|
+
},
|
|
321
|
+
info: {
|
|
322
|
+
border: 'border-blue-200 dark:border-blue-800/80',
|
|
323
|
+
bg: 'bg-white dark:bg-slate-900',
|
|
324
|
+
iconColor: 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-950/60',
|
|
325
|
+
symbol: 'ℹ',
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
var cfg = typeConfig[t.type] || typeConfig.info;
|
|
329
|
+
|
|
330
|
+
return m('div.pointer-events-auto.shadow-xl.rounded-xl.border.p-4.flex.items-start.gap-3.transition-all.transform', {
|
|
331
|
+
key: t.id,
|
|
332
|
+
class: cfg.border + ' ' + cfg.bg
|
|
333
|
+
}, [
|
|
334
|
+
m('div.w-7.h-7.rounded-lg.flex.items-center.justify-center.text-xs.font-bold.flex-shrink-0', {
|
|
335
|
+
class: cfg.iconColor
|
|
336
|
+
}, cfg.symbol),
|
|
337
|
+
m('div.flex-1.min-w-0', [
|
|
338
|
+
t.title ? m('h4.text-sm.font-semibold.text-gray-900.dark:text-slate-100', t.title) : null,
|
|
339
|
+
m('p.text-xs.text-gray-600.dark:text-slate-300.break-words', t.message)
|
|
340
|
+
]),
|
|
341
|
+
m('button.text-gray-400.hover:text-gray-600.dark:hover:text-slate-200.p-1.text-xs', {
|
|
342
|
+
onclick: function() { toastState.remove(t.id); }
|
|
343
|
+
}, '✕')
|
|
344
|
+
]);
|
|
345
|
+
})
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
var confirmState = (typeof window !== 'undefined' && window.confirmState) ? window.confirmState : {
|
|
351
|
+
current: null,
|
|
352
|
+
open: function(options) {
|
|
353
|
+
options = options || {};
|
|
354
|
+
if (typeof options === 'string') {
|
|
355
|
+
options = { message: options };
|
|
356
|
+
}
|
|
357
|
+
return new Promise(function(resolve) {
|
|
358
|
+
confirmState.current = {
|
|
359
|
+
title: options.title || 'Confirmation',
|
|
360
|
+
message: options.message || 'Are you sure you want to proceed?',
|
|
361
|
+
confirmText: options.confirmText || 'Confirm',
|
|
362
|
+
cancelText: options.cancelText || 'Cancel',
|
|
363
|
+
confirmColor: options.confirmColor || (options.danger ? 'red' : 'blue'),
|
|
364
|
+
danger: options.danger || false,
|
|
365
|
+
resolve: resolve,
|
|
366
|
+
};
|
|
367
|
+
try {
|
|
368
|
+
if (typeof m !== 'undefined' && typeof m.redraw === 'function') {
|
|
369
|
+
m.redraw();
|
|
370
|
+
}
|
|
371
|
+
} catch (e) {}
|
|
372
|
+
});
|
|
373
|
+
},
|
|
374
|
+
close: function(result) {
|
|
375
|
+
if (confirmState.current && confirmState.current.resolve) {
|
|
376
|
+
confirmState.current.resolve(Boolean(result));
|
|
377
|
+
}
|
|
378
|
+
confirmState.current = null;
|
|
379
|
+
try {
|
|
380
|
+
if (typeof m !== 'undefined' && typeof m.redraw === 'function') {
|
|
381
|
+
m.redraw();
|
|
382
|
+
}
|
|
383
|
+
} catch (e) {}
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
var confirmModal = function(options) {
|
|
388
|
+
return confirmState.open(options);
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
var ConfirmModalContainer = {
|
|
392
|
+
view: function() {
|
|
393
|
+
var cur = (typeof confirmState !== 'undefined' && confirmState.current) || null;
|
|
394
|
+
if (!cur) return null;
|
|
395
|
+
var isDanger = cur.danger || cur.confirmColor === 'red';
|
|
396
|
+
|
|
397
|
+
return m('div.fixed.inset-0.z-50.flex.items-center.justify-center.p-4', [
|
|
398
|
+
m('div.fixed.inset-0.bg-black/50.backdrop-blur-sm', {
|
|
399
|
+
onclick: function() { confirmState.close(false); }
|
|
400
|
+
}),
|
|
401
|
+
m('div.relative.bg-white.dark:bg-slate-900.rounded-xl.shadow-2xl.border.border-gray-200.dark:border-slate-700.max-w-md.w-full.p-6.z-10', [
|
|
402
|
+
m('div.flex.items-start.gap-4', [
|
|
403
|
+
m('div.w-10.h-10.rounded-full.flex.items-center.justify-center.flex-shrink-0', {
|
|
404
|
+
class: isDanger ? 'bg-red-100 dark:bg-red-950/60 text-red-600 dark:text-red-400' : 'bg-blue-100 dark:bg-blue-950/60 text-blue-600 dark:text-blue-400'
|
|
405
|
+
}, isDanger ? '⚠' : '?'),
|
|
406
|
+
m('div.flex-1', [
|
|
407
|
+
m('h3.text-base.font-bold.text-gray-900.dark:text-slate-100', cur.title),
|
|
408
|
+
m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1.5', cur.message)
|
|
409
|
+
])
|
|
410
|
+
]),
|
|
411
|
+
m('div.mt-6.flex.items-center.justify-end.gap-3', [
|
|
412
|
+
m('button.px-4.py-2.text-xs.font-medium.text-gray-700.dark:text-slate-300.bg-gray-100.dark:bg-slate-800.hover:bg-gray-200.dark:hover:bg-slate-700.rounded-lg', {
|
|
413
|
+
onclick: function() { confirmState.close(false); }
|
|
414
|
+
}, cur.cancelText),
|
|
415
|
+
m('button.px-4.py-2.text-xs.font-medium.text-white.rounded-lg.shadow-sm', {
|
|
416
|
+
class: isDanger ? 'bg-red-600 hover:bg-red-700' : 'bg-blue-600 hover:bg-blue-700',
|
|
417
|
+
onclick: function() { confirmState.close(true); }
|
|
418
|
+
}, cur.confirmText)
|
|
419
|
+
])
|
|
420
|
+
])
|
|
421
|
+
]);
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
// Convenience notification & confirmation helpers
|
|
426
|
+
function notifySuccess(msg, optionsOrTtl) {
|
|
427
|
+
return notify.success(msg, optionsOrTtl);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function notifyError(msg, optionsOrTtl) {
|
|
431
|
+
return notify.error(msg, optionsOrTtl);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function notifyWarning(msg, optionsOrTtl) {
|
|
435
|
+
return notify.warning(msg, optionsOrTtl);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function notifyInfo(msg, optionsOrTtl) {
|
|
439
|
+
return notify.info(msg, optionsOrTtl);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async function confirmAction(optionsOrMsg) {
|
|
443
|
+
return await confirmModal(optionsOrMsg);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Global window bindings
|
|
447
|
+
if (typeof window !== 'undefined') {
|
|
448
|
+
window.toastState = toastState;
|
|
449
|
+
window.confirmState = confirmState;
|
|
450
|
+
window.notify = notify;
|
|
451
|
+
window.toast = notify;
|
|
452
|
+
window.confirmModal = confirmModal;
|
|
453
|
+
window.ToastContainer = ToastContainer;
|
|
454
|
+
window.ConfirmModalContainer = ConfirmModalContainer;
|
|
455
|
+
window.Admin = window.Admin || {};
|
|
456
|
+
window.Admin.toast = notify;
|
|
457
|
+
window.Admin.confirm = confirmModal;
|
|
153
458
|
}
|
|
154
459
|
|
|
155
460
|
// Breadcrumb Component
|