webspresso 0.0.98 → 0.0.100
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 +11 -1
- package/.agents/AGENTS.md +2 -2
- package/.agents/AUTH.md +415 -0
- package/.agents/CLI.md +47 -0
- package/.agents/ERRORS.md +59 -0
- package/.agents/ORM.md +178 -4
- package/.agents/PLUGINS.md +113 -8
- package/.agents/QUEUE.md +92 -0
- package/.agents/ROUTING.md +160 -7
- package/.agents/STREAMING.md +35 -0
- package/AGENTS.md +2 -0
- package/README.md +145 -93
- package/bin/commands/add-docker.js +467 -0
- package/bin/commands/deploy-check.js +339 -0
- package/bin/webspresso.js +4 -0
- package/core/auth/index.js +123 -1
- package/core/auth/magic-link.js +170 -0
- package/core/auth/manager.js +477 -3
- package/core/auth/middleware.js +118 -5
- package/core/auth/oauth.js +517 -0
- package/core/auth/qrcode.js +499 -0
- package/core/auth/session-manager.js +255 -0
- package/core/auth/sessions.js +351 -0
- package/core/auth/tokens.js +1 -0
- package/core/auth/totp.js +507 -0
- package/core/auth/user-agent.js +161 -0
- package/core/orm/eager-loader.js +144 -10
- package/core/orm/index.js +10 -1
- package/core/orm/model.js +55 -1
- package/core/orm/query-builder.js +90 -4
- package/core/orm/repository.js +327 -16
- package/core/orm/scopes.js +3 -2
- package/core/orm/search.js +213 -0
- package/core/orm/types.js +6 -1
- package/core/queue/cron.js +544 -0
- package/core/queue/index.js +8 -1
- package/core/queue/manager.js +100 -1
- package/core/queue/scheduler.js +415 -0
- package/core/sse/index.js +19 -0
- package/core/sse/stream.js +430 -0
- package/core/ssr/stream.js +62 -2
- package/index.d.ts +794 -6
- package/index.js +48 -0
- package/package.json +7 -5
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +323 -31
- package/plugins/admin-panel/client/parts/08-record-list.js +5 -0
- package/plugins/admin-panel/client/verify-spa-parts.js +2 -0
- 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/modules/admin-users.js +1 -1
- 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 -70
- package/plugins/audit-log/api-handlers.js +114 -0
- package/plugins/audit-log/index.js +24 -0
- package/plugins/content/client/inline-edit.js +2 -46
- package/plugins/cors.js +13 -2
- package/plugins/email/auth-bridge.js +66 -0
- package/plugins/email/bundled-templates.js +15 -0
- package/plugins/email/templates/auth-magic-link.mjml +13 -0
- package/plugins/file-manager/admin-component.js +69 -53
- package/plugins/file-manager/index.js +25 -8
- package/plugins/index.js +8 -0
- package/plugins/oauth/index.js +338 -0
- package/plugins/queue/index.js +4 -0
- package/plugins/webhooks/admin-component.js +608 -0
- package/plugins/webhooks/api-handlers.js +237 -0
- package/plugins/webhooks/crypto.js +144 -0
- package/plugins/webhooks/dispatcher.js +708 -0
- package/plugins/webhooks/index.js +263 -0
- package/plugins/webhooks/matcher.js +113 -0
- package/plugins/webhooks/migration-template.js +75 -0
- package/plugins/webhooks/models.js +98 -0
- package/plugins/webhooks/sender.js +176 -0
- package/src/api/api-loader.js +1 -0
- package/src/bundle/index.js +2 -1
- package/src/discovery/file-route-parser.js +44 -1
- package/src/discovery/scan-routes.js +2 -2
- package/src/file-router.js +310 -104
- package/src/helpers.js +16 -1
- package/src/njk-frontmatter.js +3 -0
- package/src/routing/cascading.js +490 -0
- package/src/routing/middleware-resolver.js +92 -0
- package/src/server.js +6 -0
|
@@ -27,9 +27,24 @@ function shouldShowImagePreview(url, accept) {
|
|
|
27
27
|
return isImageAccept(accept) || isImageUrl(url);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
function renderUploadedFilePreview(url, { accept, readonly, onRemove, label }) {
|
|
30
|
+
function renderUploadedFilePreview(url, { accept, readonly, onRemove, onSelectLibrary, label }) {
|
|
31
31
|
if (!url) return null;
|
|
32
32
|
|
|
33
|
+
const actions = !readonly ? m('.flex.items-center.gap-3.mt-1', [
|
|
34
|
+
onSelectLibrary
|
|
35
|
+
? m('button.text-indigo-600.dark:text-indigo-400.hover:text-indigo-800.dark:hover:text-indigo-300.text-xs.font-medium', {
|
|
36
|
+
type: 'button',
|
|
37
|
+
onclick: onSelectLibrary,
|
|
38
|
+
}, '📁 Change from Library')
|
|
39
|
+
: null,
|
|
40
|
+
onRemove
|
|
41
|
+
? m('button.text-red-600.dark:text-red-400.hover:text-red-800.dark:hover:text-red-300.text-xs', {
|
|
42
|
+
type: 'button',
|
|
43
|
+
onclick: onRemove,
|
|
44
|
+
}, 'Remove')
|
|
45
|
+
: null,
|
|
46
|
+
]) : null;
|
|
47
|
+
|
|
33
48
|
if (shouldShowImagePreview(url, accept)) {
|
|
34
49
|
return m('.mt-3.flex.flex-col.items-start.gap-2', [
|
|
35
50
|
m('a.block', { href: url, target: '_blank', rel: 'noopener noreferrer' },
|
|
@@ -40,12 +55,7 @@ function renderUploadedFilePreview(url, { accept, readonly, onRemove, label }) {
|
|
|
40
55
|
})
|
|
41
56
|
),
|
|
42
57
|
m('p.text-xs.text-gray-500.dark:text-slate-400.break-all', url),
|
|
43
|
-
|
|
44
|
-
? m('button.text-red-600.dark:text-red-400.hover:text-red-800.dark:hover:text-red-300.text-sm', {
|
|
45
|
-
type: 'button',
|
|
46
|
-
onclick: onRemove,
|
|
47
|
-
}, 'Remove')
|
|
48
|
-
: null,
|
|
58
|
+
actions,
|
|
49
59
|
]);
|
|
50
60
|
}
|
|
51
61
|
|
|
@@ -55,12 +65,7 @@ function renderUploadedFilePreview(url, { accept, readonly, onRemove, label }) {
|
|
|
55
65
|
target: '_blank',
|
|
56
66
|
rel: 'noopener noreferrer',
|
|
57
67
|
}, url),
|
|
58
|
-
|
|
59
|
-
? m('button.text-red-600.dark:text-red-400.hover:text-red-800.dark:hover:text-red-300.text-sm', {
|
|
60
|
-
type: 'button',
|
|
61
|
-
onclick: onRemove,
|
|
62
|
-
}, 'Remove')
|
|
63
|
-
: null,
|
|
68
|
+
actions,
|
|
64
69
|
]);
|
|
65
70
|
}
|
|
66
71
|
|
|
@@ -141,6 +146,19 @@ module.exports = {
|
|
|
141
146
|
if (vnode.attrs.onchange) vnode.attrs.onchange('');
|
|
142
147
|
};
|
|
143
148
|
|
|
149
|
+
const openLibraryPicker = () => {
|
|
150
|
+
if (typeof window !== 'undefined' && window.__openFileManagerPicker) {
|
|
151
|
+
window.__openFileManagerPicker({
|
|
152
|
+
accept,
|
|
153
|
+
title: 'Choose from Media Library',
|
|
154
|
+
onSelect: (pickedUrl) => {
|
|
155
|
+
revokeLocalPreview(state);
|
|
156
|
+
if (vnode.attrs.onchange) vnode.attrs.onchange(pickedUrl);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
144
162
|
if (!uploadUrl) {
|
|
145
163
|
return m('.mb-4', [
|
|
146
164
|
m(
|
|
@@ -148,7 +166,14 @@ module.exports = {
|
|
|
148
166
|
label,
|
|
149
167
|
required ? m('span.text-red-500', ' *') : null
|
|
150
168
|
),
|
|
151
|
-
|
|
169
|
+
typeof window !== 'undefined' && window.__openFileManagerPicker
|
|
170
|
+
? m('.flex.items-center.gap-2.mb-3', [
|
|
171
|
+
m('button.px-3.py-1.5.bg-indigo-600.hover:bg-indigo-700.text-white.text-xs.font-medium.rounded.flex.items-center.gap-1.shadow-sm', {
|
|
172
|
+
type: 'button',
|
|
173
|
+
onclick: openLibraryPicker,
|
|
174
|
+
}, '📁 Choose from Media Library')
|
|
175
|
+
])
|
|
176
|
+
: m('p.text-xs.text-amber-700.dark:text-amber-400.mb-2', 'Upload URL is not configured.'),
|
|
152
177
|
m('input.w-full.px-3.py-2.border.rounded', {
|
|
153
178
|
type: 'text',
|
|
154
179
|
name,
|
|
@@ -161,7 +186,7 @@ module.exports = {
|
|
|
161
186
|
},
|
|
162
187
|
}),
|
|
163
188
|
value
|
|
164
|
-
? renderUploadedFilePreview(value, { accept, readonly, onRemove: clearValue, label })
|
|
189
|
+
? renderUploadedFilePreview(value, { accept, readonly, onRemove: clearValue, onSelectLibrary: openLibraryPicker, label })
|
|
165
190
|
: null,
|
|
166
191
|
]);
|
|
167
192
|
}
|
|
@@ -173,7 +198,7 @@ module.exports = {
|
|
|
173
198
|
required ? m('span.text-red-500', ' *') : null
|
|
174
199
|
),
|
|
175
200
|
displayUrl
|
|
176
|
-
? renderUploadedFilePreview(displayUrl, { accept, readonly, onRemove: clearValue, label })
|
|
201
|
+
? renderUploadedFilePreview(displayUrl, { accept, readonly, onRemove: clearValue, onSelectLibrary: openLibraryPicker, label })
|
|
177
202
|
: null,
|
|
178
203
|
m(
|
|
179
204
|
'div.border-2.border-dashed.border-gray-300.dark:border-slate-600.rounded.p-6.text-center',
|
|
@@ -200,11 +225,26 @@ module.exports = {
|
|
|
200
225
|
'p.text-gray-600.dark:text-slate-400.mb-2',
|
|
201
226
|
displayUrl ? 'Drag and drop to replace, or' : 'Drag and drop a file here, or'
|
|
202
227
|
),
|
|
203
|
-
m(
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
228
|
+
m('.flex.items-center.justify-center.gap-3.flex-wrap', [
|
|
229
|
+
m(
|
|
230
|
+
'label.text-blue-600.hover:text-blue-800.cursor-pointer',
|
|
231
|
+
{ for: 'file-input-' + name },
|
|
232
|
+
displayUrl ? 'choose another file' : 'browse'
|
|
233
|
+
),
|
|
234
|
+
typeof window !== 'undefined' && window.__openFileManagerPicker
|
|
235
|
+
? m('button.text-indigo-600.hover:text-indigo-800.dark:text-indigo-400.cursor-pointer.font-medium.flex.items-center.gap-1.text-sm', {
|
|
236
|
+
type: 'button',
|
|
237
|
+
onclick: (e) => {
|
|
238
|
+
e.preventDefault();
|
|
239
|
+
e.stopPropagation();
|
|
240
|
+
openLibraryPicker();
|
|
241
|
+
}
|
|
242
|
+
}, [
|
|
243
|
+
m('span', '•'),
|
|
244
|
+
m('span', '📁 Choose from Media Library')
|
|
245
|
+
])
|
|
246
|
+
: null,
|
|
247
|
+
]),
|
|
208
248
|
]),
|
|
209
249
|
]
|
|
210
250
|
),
|
|
@@ -280,6 +320,98 @@ module.exports = {
|
|
|
280
320
|
|
|
281
321
|
const urls = Array.isArray(value) ? value : (value ? [value] : []);
|
|
282
322
|
|
|
323
|
+
const openLibraryPicker = () => {
|
|
324
|
+
if (typeof window !== 'undefined' && window.__openFileManagerPicker) {
|
|
325
|
+
window.__openFileManagerPicker({
|
|
326
|
+
accept,
|
|
327
|
+
title: 'Add from Media Library',
|
|
328
|
+
onSelect: (pickedUrl) => {
|
|
329
|
+
const newUrls = [...urls, pickedUrl];
|
|
330
|
+
if (vnode.attrs.onchange) vnode.attrs.onchange(newUrls);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const renderGrid = () => {
|
|
337
|
+
if (urls.length === 0) return null;
|
|
338
|
+
return m('.grid.grid-cols-2.sm:grid-cols-3.md:grid-cols-4.gap-4.mb-3', urls.map((url, idx) => {
|
|
339
|
+
const isDragOver = state.dragOverIndex === idx;
|
|
340
|
+
const baseClasses = '.relative.group.border.rounded-lg.p-2.transition-all';
|
|
341
|
+
const cursorClass = readonly ? '' : '.cursor-move';
|
|
342
|
+
const borderClasses = isDragOver ? '.border-blue-500.border-2.dark:border-blue-500' : '.border-gray-200.dark:border-slate-700';
|
|
343
|
+
const bgClasses = isDragOver ? '.bg-blue-50.dark:bg-slate-800' : '.bg-gray-50.dark:bg-slate-900';
|
|
344
|
+
|
|
345
|
+
return m(baseClasses + cursorClass + borderClasses + bgClasses, {
|
|
346
|
+
draggable: !readonly,
|
|
347
|
+
ondragstart: (e) => {
|
|
348
|
+
state.draggedIndex = idx;
|
|
349
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
350
|
+
e.dataTransfer.setData('text/plain', idx);
|
|
351
|
+
},
|
|
352
|
+
ondragover: (e) => {
|
|
353
|
+
e.preventDefault();
|
|
354
|
+
e.dataTransfer.dropEffect = 'move';
|
|
355
|
+
},
|
|
356
|
+
ondragenter: (e) => {
|
|
357
|
+
state.dragOverIndex = idx;
|
|
358
|
+
},
|
|
359
|
+
ondragleave: (e) => {
|
|
360
|
+
if (state.dragOverIndex === idx) {
|
|
361
|
+
state.dragOverIndex = null;
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
ondrop: (e) => {
|
|
365
|
+
e.preventDefault();
|
|
366
|
+
const fromIdx = state.draggedIndex;
|
|
367
|
+
const toIdx = idx;
|
|
368
|
+
if (fromIdx !== null && fromIdx !== undefined && fromIdx !== toIdx) {
|
|
369
|
+
const newUrls = [...urls];
|
|
370
|
+
const [movedItem] = newUrls.splice(fromIdx, 1);
|
|
371
|
+
newUrls.splice(toIdx, 0, movedItem);
|
|
372
|
+
if (vnode.attrs.onchange) vnode.attrs.onchange(newUrls);
|
|
373
|
+
}
|
|
374
|
+
state.draggedIndex = null;
|
|
375
|
+
state.dragOverIndex = null;
|
|
376
|
+
},
|
|
377
|
+
ondragend: (e) => {
|
|
378
|
+
state.draggedIndex = null;
|
|
379
|
+
state.dragOverIndex = null;
|
|
380
|
+
}
|
|
381
|
+
}, [
|
|
382
|
+
shouldShowImagePreview(url, accept)
|
|
383
|
+
? m('a.block', { href: url, target: '_blank', rel: 'noopener noreferrer', draggable: false },
|
|
384
|
+
m('img.h-32.w-full.rounded.object-contain.bg-white.dark:bg-slate-950', {
|
|
385
|
+
src: url,
|
|
386
|
+
alt: `${label} ${idx + 1}`,
|
|
387
|
+
loading: 'lazy',
|
|
388
|
+
draggable: false,
|
|
389
|
+
})
|
|
390
|
+
)
|
|
391
|
+
: m('a.flex.items-center.justify-center.h-32.w-full.text-sm.text-indigo-600.dark:text-indigo-400.break-all.p-2', {
|
|
392
|
+
href: url,
|
|
393
|
+
target: '_blank',
|
|
394
|
+
rel: 'noopener noreferrer',
|
|
395
|
+
draggable: false,
|
|
396
|
+
}, url.substring(url.lastIndexOf('/') + 1) || url),
|
|
397
|
+
!readonly
|
|
398
|
+
? m('button.absolute.top-1.right-1.bg-red-500.text-white.rounded-full.p-1.shadow-md.opacity-0.group-hover:opacity-100.hover:bg-red-700.transition-opacity', {
|
|
399
|
+
type: 'button',
|
|
400
|
+
onclick: () => {
|
|
401
|
+
const newUrls = urls.filter((_, i) => i !== idx);
|
|
402
|
+
if (vnode.attrs.onchange) vnode.attrs.onchange(newUrls);
|
|
403
|
+
},
|
|
404
|
+
title: 'Remove file',
|
|
405
|
+
}, [
|
|
406
|
+
m('svg.w-4.h-4', { fill: 'none', stroke: 'currentColor', viewBox: '0 0 24 24' },
|
|
407
|
+
m('path', { 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'stroke-width': '2', d: 'M6 18L18 6M6 6l12 12' })
|
|
408
|
+
)
|
|
409
|
+
])
|
|
410
|
+
: null,
|
|
411
|
+
]);
|
|
412
|
+
}));
|
|
413
|
+
};
|
|
414
|
+
|
|
283
415
|
if (!uploadUrl) {
|
|
284
416
|
return m('.mb-4', [
|
|
285
417
|
m(
|
|
@@ -287,7 +419,19 @@ module.exports = {
|
|
|
287
419
|
label,
|
|
288
420
|
required ? m('span.text-red-500', ' *') : null
|
|
289
421
|
),
|
|
290
|
-
|
|
422
|
+
typeof window !== 'undefined' && window.__openFileManagerPicker
|
|
423
|
+
? m('.flex.items-center.gap-2.mb-3', [
|
|
424
|
+
m('button.px-3.py-1.5.bg-indigo-600.hover:bg-indigo-700.text-white.text-xs.font-medium.rounded.flex.items-center.gap-1.shadow-sm', {
|
|
425
|
+
type: 'button',
|
|
426
|
+
onclick: openLibraryPicker,
|
|
427
|
+
}, '📁 Add from Media Library')
|
|
428
|
+
])
|
|
429
|
+
: m('p.text-xs.text-amber-700.dark:text-amber-400.mb-2', 'Upload URL is not configured.'),
|
|
430
|
+
renderGrid(),
|
|
431
|
+
m('input[type=hidden]', {
|
|
432
|
+
name,
|
|
433
|
+
value: JSON.stringify(urls),
|
|
434
|
+
}),
|
|
291
435
|
]);
|
|
292
436
|
}
|
|
293
437
|
|
|
@@ -298,83 +442,7 @@ module.exports = {
|
|
|
298
442
|
required ? m('span.text-red-500', ' *') : null
|
|
299
443
|
),
|
|
300
444
|
|
|
301
|
-
|
|
302
|
-
? m('.grid.grid-cols-2.sm:grid-cols-3.md:grid-cols-4.gap-4.mb-3', urls.map((url, idx) => {
|
|
303
|
-
const isDragOver = state.dragOverIndex === idx;
|
|
304
|
-
const baseClasses = '.relative.group.border.rounded-lg.p-2.transition-all';
|
|
305
|
-
const cursorClass = readonly ? '' : '.cursor-move';
|
|
306
|
-
const borderClasses = isDragOver ? '.border-blue-500.border-2.dark:border-blue-500' : '.border-gray-200.dark:border-slate-700';
|
|
307
|
-
const bgClasses = isDragOver ? '.bg-blue-50.dark:bg-slate-800' : '.bg-gray-50.dark:bg-slate-900';
|
|
308
|
-
|
|
309
|
-
return m(baseClasses + cursorClass + borderClasses + bgClasses, {
|
|
310
|
-
draggable: !readonly,
|
|
311
|
-
ondragstart: (e) => {
|
|
312
|
-
state.draggedIndex = idx;
|
|
313
|
-
e.dataTransfer.effectAllowed = 'move';
|
|
314
|
-
e.dataTransfer.setData('text/plain', idx);
|
|
315
|
-
},
|
|
316
|
-
ondragover: (e) => {
|
|
317
|
-
e.preventDefault();
|
|
318
|
-
e.dataTransfer.dropEffect = 'move';
|
|
319
|
-
},
|
|
320
|
-
ondragenter: (e) => {
|
|
321
|
-
state.dragOverIndex = idx;
|
|
322
|
-
},
|
|
323
|
-
ondragleave: (e) => {
|
|
324
|
-
if (state.dragOverIndex === idx) {
|
|
325
|
-
state.dragOverIndex = null;
|
|
326
|
-
}
|
|
327
|
-
},
|
|
328
|
-
ondrop: (e) => {
|
|
329
|
-
e.preventDefault();
|
|
330
|
-
const fromIdx = state.draggedIndex;
|
|
331
|
-
const toIdx = idx;
|
|
332
|
-
if (fromIdx !== null && fromIdx !== undefined && fromIdx !== toIdx) {
|
|
333
|
-
const newUrls = [...urls];
|
|
334
|
-
const [movedItem] = newUrls.splice(fromIdx, 1);
|
|
335
|
-
newUrls.splice(toIdx, 0, movedItem);
|
|
336
|
-
if (vnode.attrs.onchange) vnode.attrs.onchange(newUrls);
|
|
337
|
-
}
|
|
338
|
-
state.draggedIndex = null;
|
|
339
|
-
state.dragOverIndex = null;
|
|
340
|
-
},
|
|
341
|
-
ondragend: (e) => {
|
|
342
|
-
state.draggedIndex = null;
|
|
343
|
-
state.dragOverIndex = null;
|
|
344
|
-
}
|
|
345
|
-
}, [
|
|
346
|
-
shouldShowImagePreview(url, accept)
|
|
347
|
-
? m('a.block', { href: url, target: '_blank', rel: 'noopener noreferrer', draggable: false },
|
|
348
|
-
m('img.h-32.w-full.rounded.object-contain.bg-white.dark:bg-slate-950', {
|
|
349
|
-
src: url,
|
|
350
|
-
alt: `${label} ${idx + 1}`,
|
|
351
|
-
loading: 'lazy',
|
|
352
|
-
draggable: false,
|
|
353
|
-
})
|
|
354
|
-
)
|
|
355
|
-
: m('a.flex.items-center.justify-center.h-32.w-full.text-sm.text-indigo-600.dark:text-indigo-400.break-all.p-2', {
|
|
356
|
-
href: url,
|
|
357
|
-
target: '_blank',
|
|
358
|
-
rel: 'noopener noreferrer',
|
|
359
|
-
draggable: false,
|
|
360
|
-
}, url.substring(url.lastIndexOf('/') + 1) || url),
|
|
361
|
-
!readonly
|
|
362
|
-
? m('button.absolute.top-1.right-1.bg-red-500.text-white.rounded-full.p-1.shadow-md.opacity-0.group-hover:opacity-100.hover:bg-red-700.transition-opacity', {
|
|
363
|
-
type: 'button',
|
|
364
|
-
onclick: () => {
|
|
365
|
-
const newUrls = urls.filter((_, i) => i !== idx);
|
|
366
|
-
if (vnode.attrs.onchange) vnode.attrs.onchange(newUrls);
|
|
367
|
-
},
|
|
368
|
-
title: 'Remove file',
|
|
369
|
-
}, [
|
|
370
|
-
m('svg.w-4.h-4', { fill: 'none', stroke: 'currentColor', viewBox: '0 0 24 24' },
|
|
371
|
-
m('path', { 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'stroke-width': '2', d: 'M6 18L18 6M6 6l12 12' })
|
|
372
|
-
)
|
|
373
|
-
])
|
|
374
|
-
: null,
|
|
375
|
-
]);
|
|
376
|
-
}))
|
|
377
|
-
: null,
|
|
445
|
+
renderGrid(),
|
|
378
446
|
|
|
379
447
|
m(
|
|
380
448
|
'div.border-2.border-dashed.border-gray-300.dark:border-slate-600.rounded.p-6.text-center',
|
|
@@ -405,11 +473,26 @@ module.exports = {
|
|
|
405
473
|
'p.text-gray-600.dark:text-slate-400.mb-2',
|
|
406
474
|
'Drag and drop files here, or'
|
|
407
475
|
),
|
|
408
|
-
m(
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
476
|
+
m('.flex.items-center.justify-center.gap-3.flex-wrap', [
|
|
477
|
+
m(
|
|
478
|
+
'label.text-blue-600.hover:text-blue-800.cursor-pointer',
|
|
479
|
+
{ for: 'file-input-multiple-' + name },
|
|
480
|
+
'browse'
|
|
481
|
+
),
|
|
482
|
+
typeof window !== 'undefined' && window.__openFileManagerPicker
|
|
483
|
+
? m('button.text-indigo-600.hover:text-indigo-800.dark:text-indigo-400.cursor-pointer.font-medium.flex.items-center.gap-1.text-sm', {
|
|
484
|
+
type: 'button',
|
|
485
|
+
onclick: (e) => {
|
|
486
|
+
e.preventDefault();
|
|
487
|
+
e.stopPropagation();
|
|
488
|
+
openLibraryPicker();
|
|
489
|
+
}
|
|
490
|
+
}, [
|
|
491
|
+
m('span', '•'),
|
|
492
|
+
m('span', '📁 Add from Media Library')
|
|
493
|
+
])
|
|
494
|
+
: null,
|
|
495
|
+
]),
|
|
413
496
|
]),
|
|
414
497
|
]),
|
|
415
498
|
]
|
|
@@ -79,7 +79,9 @@ function initializeDefaultRenderers() {
|
|
|
79
79
|
registerFieldRenderer('rich-text', richTextRenderer.RichTextField);
|
|
80
80
|
registerFieldRenderer('file-upload', fileUploadRenderer.FileUploadField);
|
|
81
81
|
registerFieldRenderer('file', fileUploadRenderer.FileUploadField);
|
|
82
|
+
registerFieldRenderer('image', fileUploadRenderer.FileUploadField);
|
|
82
83
|
registerFieldRenderer('files', fileUploadRenderer.FilesUploadField);
|
|
84
|
+
registerFieldRenderer('files-upload', fileUploadRenderer.FilesUploadField);
|
|
83
85
|
|
|
84
86
|
// Relation types
|
|
85
87
|
registerFieldRenderer('belongsTo', relationRenderers.BelongsToField);
|
|
@@ -692,7 +692,7 @@ const AdminUsersPage = {
|
|
|
692
692
|
m('input.w-full.px-3.py-2.border.border-gray-300.dark:border-slate-600.rounded-lg.bg-white.dark:bg-slate-900.text-gray-900.dark:text-slate-100.focus:ring-2.focus:ring-blue-500', {
|
|
693
693
|
type: 'text',
|
|
694
694
|
required: true,
|
|
695
|
-
placeholder: 'e.g.
|
|
695
|
+
placeholder: 'e.g. Jane Doe',
|
|
696
696
|
value: s.formName,
|
|
697
697
|
oninput: (e) => { s.formName = e.target.value; },
|
|
698
698
|
}),
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Key Plugin — Cryptographic Utilities
|
|
3
|
+
* Zero external dependencies (uses native Node.js crypto)
|
|
4
|
+
* @module plugins/api-key/crypto
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const crypto = require('crypto');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Compare two strings in constant time to prevent timing attacks.
|
|
13
|
+
* Uses dummy buffer comparison if lengths differ to avoid leaking length via response timing.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} a
|
|
16
|
+
* @param {string} b
|
|
17
|
+
* @returns {boolean}
|
|
18
|
+
*/
|
|
19
|
+
function safeCompare(a, b) {
|
|
20
|
+
if (typeof a !== 'string' || typeof b !== 'string') {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const bufA = Buffer.from(a, 'utf8');
|
|
25
|
+
const bufB = Buffer.from(b, 'utf8');
|
|
26
|
+
|
|
27
|
+
if (bufA.length !== bufB.length) {
|
|
28
|
+
// Perform dummy timing-safe comparison with itself to preserve constant execution time
|
|
29
|
+
crypto.timingSafeEqual(bufA, bufA);
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return crypto.timingSafeEqual(bufA, bufB);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Hash a raw API key using SHA-256
|
|
38
|
+
*
|
|
39
|
+
* @param {string} rawKey
|
|
40
|
+
* @returns {string} Hex-encoded SHA-256 hash (64 characters)
|
|
41
|
+
*/
|
|
42
|
+
function hashKey(rawKey) {
|
|
43
|
+
if (typeof rawKey !== 'string' || rawKey.length === 0) {
|
|
44
|
+
return '';
|
|
45
|
+
}
|
|
46
|
+
return crypto.createHash('sha256').update(rawKey, 'utf8').digest('hex');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Mask an API key for safe UI display and logs.
|
|
51
|
+
* Shows prefix + '...' + last 4 characters.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} rawKey
|
|
54
|
+
* @param {string} [prefix='wsp_live_']
|
|
55
|
+
* @returns {string}
|
|
56
|
+
*/
|
|
57
|
+
function maskKey(rawKey, prefix = 'wsp_live_') {
|
|
58
|
+
if (!rawKey || typeof rawKey !== 'string') {
|
|
59
|
+
return '****';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (rawKey.length <= 8) {
|
|
63
|
+
return '****';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const last4 = rawKey.slice(-4);
|
|
67
|
+
const effectivePrefix = rawKey.startsWith(prefix) ? prefix : rawKey.slice(0, Math.min(prefix.length, 9));
|
|
68
|
+
return `${effectivePrefix}...${last4}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Generate a cryptographically secure API key
|
|
73
|
+
*
|
|
74
|
+
* @param {Object} [options]
|
|
75
|
+
* @param {string} [options.prefix='wsp_live_'] - Prefix for the key
|
|
76
|
+
* @param {number} [options.byteLength=24] - Number of random bytes (produces 32 base64url chars)
|
|
77
|
+
* @returns {{ key: string, hash: string, hint: string, prefix: string }}
|
|
78
|
+
*/
|
|
79
|
+
function generateKey(options = {}) {
|
|
80
|
+
const prefix = typeof options.prefix === 'string' ? options.prefix : 'wsp_live_';
|
|
81
|
+
const byteLength = typeof options.byteLength === 'number' && options.byteLength > 0 ? options.byteLength : 24;
|
|
82
|
+
|
|
83
|
+
const randomPart = crypto.randomBytes(byteLength).toString('base64url');
|
|
84
|
+
const key = `${prefix}${randomPart}`;
|
|
85
|
+
const hash = hashKey(key);
|
|
86
|
+
const hint = maskKey(key, prefix);
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
key,
|
|
90
|
+
hash,
|
|
91
|
+
hint,
|
|
92
|
+
prefix,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = {
|
|
97
|
+
safeCompare,
|
|
98
|
+
hashKey,
|
|
99
|
+
maskKey,
|
|
100
|
+
generateKey,
|
|
101
|
+
};
|