webspresso 0.0.99 → 0.0.101
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 +47 -4
- package/.agents/AGENTS.md +4 -2
- package/.agents/AUTH.md +309 -0
- package/.agents/CLI.md +128 -0
- package/.agents/EMAIL.md +84 -2
- package/.agents/ERRORS.md +59 -0
- package/.agents/EVENTS.md +179 -0
- package/.agents/ORM.md +149 -0
- package/.agents/PLUGINS.md +44 -7
- package/.agents/QUEUE.md +18 -10
- package/.agents/ROUTING.md +97 -1
- 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/db-makemigrations.js +83 -0
- package/bin/commands/deploy-check.js +339 -0
- package/bin/commands/list.js +96 -0
- package/bin/commands/make-command.js +93 -0
- package/bin/commands/make-event.js +80 -0
- package/bin/commands/make-factory.js +72 -0
- package/bin/commands/make-mail.js +93 -0
- package/bin/commands/make-policy.js +106 -0
- package/bin/commands/run.js +195 -0
- package/bin/webspresso.js +21 -0
- package/core/auth/bouncer.js +196 -0
- package/core/auth/index.js +73 -1
- package/core/auth/magic-link.js +170 -0
- package/core/auth/manager.js +343 -3
- package/core/auth/middleware.js +45 -6
- 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 +4 -1
- package/core/auth/user-agent.js +161 -0
- package/core/commands/define-command.js +48 -0
- package/core/commands/index.js +18 -0
- package/core/commands/registry.js +156 -0
- package/core/commands/runner.js +76 -0
- package/core/compression/compressible.js +1 -1
- package/core/compression/middleware.js +35 -251
- package/core/content/service.js +8 -2
- package/core/events/discovery.js +142 -0
- package/core/events/dispatcher.js +552 -0
- package/core/events/index.js +23 -0
- package/core/events/signal.js +92 -0
- package/core/forms/flash.js +234 -0
- package/core/forms/form-builder.js +214 -0
- package/core/forms/index.js +16 -0
- package/core/mail/index.js +23 -0
- package/core/mail/mailable.js +250 -0
- package/core/mail/mailer.js +184 -0
- package/core/mail/transports.js +238 -0
- package/core/orm/factory.js +250 -0
- package/core/orm/index.js +8 -1
- package/core/orm/migrations/diff.js +185 -0
- package/core/orm/migrations/index.js +22 -4
- package/core/orm/model.js +55 -1
- package/core/orm/repository.js +283 -13
- package/core/orm/scopes.js +3 -2
- package/core/orm/types.js +6 -1
- package/core/queue/cron.js +252 -39
- package/core/queue/manager.js +20 -2
- package/core/queue/scheduler.js +115 -59
- package/core/ssr/stream.js +62 -2
- package/index.d.ts +540 -14
- package/index.js +44 -0
- package/package.json +11 -5
- package/plugins/admin-panel/api.js +509 -3
- package/plugins/admin-panel/index.js +17 -0
- package/plugins/admin-panel/modules/admin-users.js +1 -1
- package/plugins/audit-log/admin-component.js +54 -54
- package/plugins/audit-log/index.js +2 -2
- package/plugins/content/client/inline-edit.js +2 -46
- package/plugins/content/models/content-entry.js +4 -3
- package/plugins/content/models/content-type.js +4 -3
- package/plugins/cors.js +15 -4
- package/plugins/email/auth-bridge.js +66 -0
- package/plugins/email/bundled-templates.js +15 -0
- package/plugins/email/index.js +48 -0
- package/plugins/email/preview-handler.js +555 -0
- package/plugins/email/service.js +23 -12
- package/plugins/email/templates/auth-magic-link.mjml +13 -0
- package/plugins/email/transport.js +30 -11
- package/plugins/file-manager/admin-component.js +49 -49
- package/plugins/file-manager/index.js +4 -4
- package/plugins/index.js +2 -0
- package/plugins/redirect/index.js +1 -1
- 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 -1
- package/src/app-context.js +48 -6
- package/src/bundle/index.js +2 -1
- package/src/dev-reload/index.js +9 -1
- package/src/discovery/scan-routes.js +10 -1
- package/src/file-router.js +173 -4
- package/src/helpers.js +150 -1
- package/src/njk-frontmatter.js +3 -0
- package/src/pages/page-loader.js +1 -1
- package/src/routing/cascading.js +120 -4
- package/src/server.js +180 -26
|
@@ -487,6 +487,23 @@ function adminPanelPlugin(options = {}) {
|
|
|
487
487
|
// Query API routes (auth required)
|
|
488
488
|
ctx.addRoute('get', `${adminPath}/api/models/:model/queries/:query`, requireAuth, apiHandlers.queryHandler);
|
|
489
489
|
|
|
490
|
+
// Inlines (Django Inlines-inspired)
|
|
491
|
+
ctx.addRoute('get', `${adminPath}/api/models/:model/records/:id/inlines/:inlineModel`, requireAuth, apiHandlers.inlineListHandler);
|
|
492
|
+
ctx.addRoute('post', `${adminPath}/api/models/:model/records/:id/inlines/:inlineModel`, requireAuth, apiHandlers.inlineCreateHandler);
|
|
493
|
+
ctx.addRoute('put', `${adminPath}/api/models/:model/records/:id/inlines/:inlineModel/:inlineId`, requireAuth, apiHandlers.inlineUpdateHandler);
|
|
494
|
+
ctx.addRoute('delete', `${adminPath}/api/models/:model/records/:id/inlines/:inlineModel/:inlineId`, requireAuth, apiHandlers.inlineDeleteHandler);
|
|
495
|
+
|
|
496
|
+
// List Editable (Django list_editable-inspired)
|
|
497
|
+
ctx.addRoute('patch', `${adminPath}/api/models/:model/records/:id/quick-edit`, requireAuth, apiHandlers.quickEditHandler);
|
|
498
|
+
ctx.addRoute('patch', `${adminPath}/api/models/:model/records/batch-edit`, requireAuth, apiHandlers.batchEditHandler);
|
|
499
|
+
|
|
500
|
+
// Date Hierarchy & Filter Facets (Django date_hierarchy & list_filter-inspired)
|
|
501
|
+
ctx.addRoute('get', `${adminPath}/api/models/:model/date-hierarchy`, requireAuth, apiHandlers.dateHierarchyHandler);
|
|
502
|
+
ctx.addRoute('get', `${adminPath}/api/models/:model/filter-facets`, requireAuth, apiHandlers.filterFacetsHandler);
|
|
503
|
+
|
|
504
|
+
// Model Actions (Django Actions-inspired)
|
|
505
|
+
ctx.addRoute('post', `${adminPath}/api/models/:model/actions/:actionId`, requireAuth, apiHandlers.modelActionHandler);
|
|
506
|
+
|
|
490
507
|
// Data exchange (Excel export & CSV/XLSX import) routes
|
|
491
508
|
const dataExchangeConfig = typeof dataExchangeOption === 'object' ? dataExchangeOption : { enabled: dataExchangeOption !== false };
|
|
492
509
|
if (dataExchangeConfig.enabled !== false && !ctx.app?._dataExchangeRoutesRegistered) {
|
|
@@ -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
|
}),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Audit log admin page — Mithril component source (string)
|
|
3
|
-
* Features dual Table & Timeline View
|
|
3
|
+
* Features dual Table & Timeline View with record focus & diff metadata
|
|
4
4
|
* @module plugins/audit-log/admin-component
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -26,11 +26,11 @@ function generateAuditLogComponent(options = {}) {
|
|
|
26
26
|
var diffHour = Math.floor(diffMin / 60);
|
|
27
27
|
var diffDay = Math.floor(diffHour / 24);
|
|
28
28
|
|
|
29
|
-
if (diffSec < 60) return '
|
|
30
|
-
if (diffMin < 60) return diffMin + '
|
|
31
|
-
if (diffHour < 24) return diffHour + '
|
|
32
|
-
if (diffDay === 1) return '
|
|
33
|
-
if (diffDay < 30) return diffDay + '
|
|
29
|
+
if (diffSec < 60) return 'Just now';
|
|
30
|
+
if (diffMin < 60) return diffMin + 'm ago';
|
|
31
|
+
if (diffHour < 24) return diffHour + 'h ago';
|
|
32
|
+
if (diffDay === 1) return 'Yesterday';
|
|
33
|
+
if (diffDay < 30) return diffDay + 'd ago';
|
|
34
34
|
return d.toLocaleDateString();
|
|
35
35
|
} catch (e) {
|
|
36
36
|
return dateStr;
|
|
@@ -41,7 +41,7 @@ function generateAuditLogComponent(options = {}) {
|
|
|
41
41
|
switch (action) {
|
|
42
42
|
case 'create':
|
|
43
43
|
return {
|
|
44
|
-
label: '
|
|
44
|
+
label: 'CREATED',
|
|
45
45
|
color: 'emerald',
|
|
46
46
|
badgeClass: 'bg-emerald-50 text-emerald-700 border-emerald-200 dark:bg-emerald-950/40 dark:text-emerald-300 dark:border-emerald-800',
|
|
47
47
|
dotClass: 'bg-emerald-500 text-white ring-4 ring-emerald-100 dark:ring-emerald-950/60',
|
|
@@ -49,7 +49,7 @@ function generateAuditLogComponent(options = {}) {
|
|
|
49
49
|
};
|
|
50
50
|
case 'update':
|
|
51
51
|
return {
|
|
52
|
-
label: '
|
|
52
|
+
label: 'UPDATED',
|
|
53
53
|
color: 'blue',
|
|
54
54
|
badgeClass: 'bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-950/40 dark:text-blue-300 dark:border-blue-800',
|
|
55
55
|
dotClass: 'bg-blue-500 text-white ring-4 ring-blue-100 dark:ring-blue-950/60',
|
|
@@ -57,7 +57,7 @@ function generateAuditLogComponent(options = {}) {
|
|
|
57
57
|
};
|
|
58
58
|
case 'delete':
|
|
59
59
|
return {
|
|
60
|
-
label: '
|
|
60
|
+
label: 'DELETED',
|
|
61
61
|
color: 'rose',
|
|
62
62
|
badgeClass: 'bg-rose-50 text-rose-700 border-rose-200 dark:bg-rose-950/40 dark:text-rose-300 dark:border-rose-800',
|
|
63
63
|
dotClass: 'bg-rose-500 text-white ring-4 ring-rose-100 dark:ring-rose-950/60',
|
|
@@ -65,7 +65,7 @@ function generateAuditLogComponent(options = {}) {
|
|
|
65
65
|
};
|
|
66
66
|
case 'restore':
|
|
67
67
|
return {
|
|
68
|
-
label: '
|
|
68
|
+
label: 'RESTORED',
|
|
69
69
|
color: 'amber',
|
|
70
70
|
badgeClass: 'bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-950/40 dark:text-amber-300 dark:border-amber-800',
|
|
71
71
|
dotClass: 'bg-amber-500 text-white ring-4 ring-amber-100 dark:ring-amber-950/60',
|
|
@@ -73,7 +73,7 @@ function generateAuditLogComponent(options = {}) {
|
|
|
73
73
|
};
|
|
74
74
|
default:
|
|
75
75
|
return {
|
|
76
|
-
label: (action || '
|
|
76
|
+
label: (action || 'ACTION').toUpperCase(),
|
|
77
77
|
color: 'gray',
|
|
78
78
|
badgeClass: 'bg-gray-100 text-gray-700 border-gray-200 dark:bg-slate-800 dark:text-slate-300 dark:border-slate-700',
|
|
79
79
|
dotClass: 'bg-gray-500 text-white ring-4 ring-gray-100 dark:ring-slate-800',
|
|
@@ -224,10 +224,10 @@ function generateAuditLogComponent(options = {}) {
|
|
|
224
224
|
m('span.p-2.rounded-lg.bg-indigo-50.text-indigo-600.dark:bg-indigo-950/40.dark:text-indigo-400',
|
|
225
225
|
renderSvgIcon('clock', 'w-5 h-5')
|
|
226
226
|
),
|
|
227
|
-
m('h1.text-2xl.font-bold.text-gray-900.dark:text-slate-100', '
|
|
227
|
+
m('h1.text-2xl.font-bold.text-gray-900.dark:text-slate-100', 'Audit Log & Timeline'),
|
|
228
228
|
]),
|
|
229
229
|
m('p.text-sm.text-gray-500.dark:text-slate-400.mt-1',
|
|
230
|
-
'
|
|
230
|
+
'Audit trail of all administrative CRUD operations and model mutations.'
|
|
231
231
|
),
|
|
232
232
|
]),
|
|
233
233
|
|
|
@@ -241,7 +241,7 @@ function generateAuditLogComponent(options = {}) {
|
|
|
241
241
|
onclick: function() { viewMode = 'timeline'; }
|
|
242
242
|
}, [
|
|
243
243
|
renderSvgIcon('clock', 'w-3.5 h-3.5'),
|
|
244
|
-
m('span', '
|
|
244
|
+
m('span', 'Timeline'),
|
|
245
245
|
]),
|
|
246
246
|
m('button.px-3.py-1.5.text-xs.font-medium.rounded-md.flex.items-center.gap-1.5.transition-all', {
|
|
247
247
|
class: viewMode === 'table'
|
|
@@ -250,22 +250,22 @@ function generateAuditLogComponent(options = {}) {
|
|
|
250
250
|
onclick: function() { viewMode = 'table'; }
|
|
251
251
|
}, [
|
|
252
252
|
renderSvgIcon('table', 'w-3.5 h-3.5'),
|
|
253
|
-
m('span', '
|
|
253
|
+
m('span', 'Table'),
|
|
254
254
|
]),
|
|
255
255
|
]),
|
|
256
256
|
|
|
257
257
|
// Purge button
|
|
258
258
|
m('button.px-3.py-1.5.text-xs.font-medium.border.border-rose-200.dark:border-rose-900.text-rose-700.dark:text-rose-400.rounded-lg.hover:bg-rose-50.dark:hover:bg-rose-950/40.transition-colors', {
|
|
259
259
|
onclick: function() {
|
|
260
|
-
if (!confirm('
|
|
260
|
+
if (!confirm('Delete audit logs older than 90 days?')) return;
|
|
261
261
|
api.post(API + '/purge', { days: 90 }).then(function() {
|
|
262
262
|
page = 1;
|
|
263
263
|
load();
|
|
264
264
|
}).catch(function(e) {
|
|
265
|
-
alert('
|
|
265
|
+
alert('Purge error: ' + (e.message || String(e)));
|
|
266
266
|
});
|
|
267
267
|
},
|
|
268
|
-
}, '
|
|
268
|
+
}, 'Purge Older than 90 Days'),
|
|
269
269
|
]),
|
|
270
270
|
]),
|
|
271
271
|
|
|
@@ -275,16 +275,16 @@ function generateAuditLogComponent(options = {}) {
|
|
|
275
275
|
m('span.w-9.h-9.rounded-full.bg-indigo-600.text-white.flex.items-center.justify-center.font-bold.text-sm', '📌'),
|
|
276
276
|
m('div', [
|
|
277
277
|
m('h3.text-sm.font-semibold.text-indigo-950.dark:text-indigo-200',
|
|
278
|
-
(filterModel ? filterModel + ' #' : '
|
|
278
|
+
'Change Timeline for ' + (filterModel ? filterModel + ' #' : 'Record #') + filterRecordId
|
|
279
279
|
),
|
|
280
280
|
m('p.text-xs.text-indigo-700.dark:text-indigo-400',
|
|
281
|
-
'
|
|
281
|
+
'Filtering all creation, update, and deletion history for this record.'
|
|
282
282
|
),
|
|
283
283
|
]),
|
|
284
284
|
]),
|
|
285
285
|
m('button.px-3.py-1.5.text-xs.font-medium.bg-white.dark:bg-slate-800.text-indigo-700.dark:text-indigo-300.border.border-indigo-200.dark:border-indigo-700.rounded-lg.hover:bg-indigo-50.dark:hover:bg-indigo-900/60.transition-colors', {
|
|
286
286
|
onclick: clearFocus,
|
|
287
|
-
}, '✕
|
|
287
|
+
}, '✕ Clear Filter'),
|
|
288
288
|
]) : null,
|
|
289
289
|
|
|
290
290
|
// 3. Search & Filter Controls Card
|
|
@@ -295,7 +295,7 @@ function generateAuditLogComponent(options = {}) {
|
|
|
295
295
|
m('label.block.text-xs.font-medium.text-gray-600.dark:text-slate-400.mb-1', 'Model'),
|
|
296
296
|
m('input.w-full.border.border-gray-300.dark:border-slate-600.bg-white.dark:bg-slate-900.text-gray-900.dark:text-slate-100.rounded-lg.px-3.py-1.5.text-sm.focus:ring-2.focus:ring-indigo-500.focus:outline-none', {
|
|
297
297
|
value: filterModel,
|
|
298
|
-
placeholder: '
|
|
298
|
+
placeholder: 'e.g. Post, User',
|
|
299
299
|
oninput: function(e) { filterModel = e.target.value; },
|
|
300
300
|
onkeydown: function(e) { if (e.key === 'Enter') { page = 1; load(); } },
|
|
301
301
|
}),
|
|
@@ -303,10 +303,10 @@ function generateAuditLogComponent(options = {}) {
|
|
|
303
303
|
|
|
304
304
|
// Record ID Filter
|
|
305
305
|
m('div', [
|
|
306
|
-
m('label.block.text-xs.font-medium.text-gray-600.dark:text-slate-400.mb-1', '
|
|
306
|
+
m('label.block.text-xs.font-medium.text-gray-600.dark:text-slate-400.mb-1', 'Record ID'),
|
|
307
307
|
m('input.w-full.border.border-gray-300.dark:border-slate-600.bg-white.dark:bg-slate-900.text-gray-900.dark:text-slate-100.rounded-lg.px-3.py-1.5.text-sm.focus:ring-2.focus:ring-indigo-500.focus:outline-none', {
|
|
308
308
|
value: filterRecordId,
|
|
309
|
-
placeholder: '
|
|
309
|
+
placeholder: 'e.g. 42',
|
|
310
310
|
oninput: function(e) { filterRecordId = e.target.value; },
|
|
311
311
|
onkeydown: function(e) { if (e.key === 'Enter') { page = 1; load(); } },
|
|
312
312
|
}),
|
|
@@ -314,26 +314,26 @@ function generateAuditLogComponent(options = {}) {
|
|
|
314
314
|
|
|
315
315
|
// Action Filter
|
|
316
316
|
m('div', [
|
|
317
|
-
m('label.block.text-xs.font-medium.text-gray-600.dark:text-slate-400.mb-1', '
|
|
317
|
+
m('label.block.text-xs.font-medium.text-gray-600.dark:text-slate-400.mb-1', 'Action'),
|
|
318
318
|
m('select.w-full.border.border-gray-300.dark:border-slate-600.bg-white.dark:bg-slate-900.text-gray-900.dark:text-slate-100.rounded-lg.px-3.py-1.5.text-sm.focus:ring-2.focus:ring-indigo-500.focus:outline-none', {
|
|
319
319
|
value: filterAction,
|
|
320
320
|
onchange: function(e) { filterAction = e.target.value; page = 1; load(); },
|
|
321
321
|
}, [
|
|
322
|
-
m('option', { value: '' }, '
|
|
323
|
-
m('option', { value: 'create' }, '
|
|
324
|
-
m('option', { value: 'update' }, '
|
|
325
|
-
m('option', { value: 'delete' }, '
|
|
326
|
-
m('option', { value: 'restore' }, '
|
|
322
|
+
m('option', { value: '' }, 'All Actions'),
|
|
323
|
+
m('option', { value: 'create' }, 'Creation (create)'),
|
|
324
|
+
m('option', { value: 'update' }, 'Update (update)'),
|
|
325
|
+
m('option', { value: 'delete' }, 'Deletion (delete)'),
|
|
326
|
+
m('option', { value: 'restore' }, 'Restore (restore)'),
|
|
327
327
|
]),
|
|
328
328
|
]),
|
|
329
329
|
|
|
330
330
|
// Search Filter (Actor, Path)
|
|
331
331
|
m('div', [
|
|
332
|
-
m('label.block.text-xs.font-medium.text-gray-600.dark:text-slate-400.mb-1', '
|
|
332
|
+
m('label.block.text-xs.font-medium.text-gray-600.dark:text-slate-400.mb-1', 'Search'),
|
|
333
333
|
m('div.relative', [
|
|
334
334
|
m('input.w-full.border.border-gray-300.dark:border-slate-600.bg-white.dark:bg-slate-900.text-gray-900.dark:text-slate-100.rounded-lg.pl-8.pr-3.py-1.5.text-sm.focus:ring-2.focus:ring-indigo-500.focus:outline-none', {
|
|
335
335
|
value: filterSearch,
|
|
336
|
-
placeholder: '
|
|
336
|
+
placeholder: 'Email, IP or path...',
|
|
337
337
|
oninput: function(e) { filterSearch = e.target.value; },
|
|
338
338
|
onkeydown: function(e) { if (e.key === 'Enter') { page = 1; load(); } },
|
|
339
339
|
}),
|
|
@@ -360,19 +360,19 @@ function generateAuditLogComponent(options = {}) {
|
|
|
360
360
|
// 4. Main Content Area (Timeline vs Table)
|
|
361
361
|
loading ? m('div.py-16.text-center.space-y-3', [
|
|
362
362
|
m('div.inline-block.animate-spin.rounded-full.h-8.w-8.border-4.border-indigo-600.border-t-transparent'),
|
|
363
|
-
m('p.text-sm.text-gray-500.dark:text-slate-400', '
|
|
363
|
+
m('p.text-sm.text-gray-500.dark:text-slate-400', 'Loading audit logs...'),
|
|
364
364
|
]) : error ? m('div.p-6.rounded-xl.bg-red-50.dark:bg-red-950/40.border.border-red-200.dark:border-red-800.text-red-700.dark:text-red-300', [
|
|
365
|
-
m('h4.font-semibold.text-sm', '
|
|
365
|
+
m('h4.font-semibold.text-sm', 'Failed to Load Records'),
|
|
366
366
|
m('p.text-xs.mt-1', error),
|
|
367
367
|
]) : rows.length === 0 ? m('div.py-16.text-center.bg-white.dark:bg-slate-800.rounded-xl.border.border-dashed.border-gray-300.dark:border-slate-700.p-8', [
|
|
368
368
|
m('div.w-12.h-12.mx-auto.rounded-full.bg-gray-100.dark:bg-slate-700.flex.items-center.justify-center.text-gray-400.mb-3',
|
|
369
369
|
renderSvgIcon('clock', 'w-6 h-6')
|
|
370
370
|
),
|
|
371
|
-
m('h3.text-base.font-medium.text-gray-900.dark:text-slate-100', '
|
|
372
|
-
m('p.text-sm.text-gray-500.dark:text-slate-400.mt-1', '
|
|
371
|
+
m('h3.text-base.font-medium.text-gray-900.dark:text-slate-100', 'No Records Found'),
|
|
372
|
+
m('p.text-sm.text-gray-500.dark:text-slate-400.mt-1', 'No audit records found matching the selected filters.'),
|
|
373
373
|
(filterModel || filterAction || filterRecordId || filterSearch) ? m('button.mt-4.text-xs.text-indigo-600.dark:text-indigo-400.hover:underline', {
|
|
374
374
|
onclick: clearAllFilters,
|
|
375
|
-
}, '
|
|
375
|
+
}, 'Clear filters and try again') : null,
|
|
376
376
|
]) : viewMode === 'timeline' ? (
|
|
377
377
|
// ==================== TIMELINE VIEW ====================
|
|
378
378
|
m('div.space-y-6', [
|
|
@@ -403,7 +403,7 @@ function generateAuditLogComponent(options = {}) {
|
|
|
403
403
|
|
|
404
404
|
// Model & ID Tag (Clickable to focus)
|
|
405
405
|
m('button.px-2.5.py-0.5.text-xs.font-medium.bg-gray-100.dark:bg-slate-700.text-gray-800.dark:text-slate-200.rounded-md.hover:bg-indigo-100.dark:hover:bg-indigo-900/60.hover:text-indigo-700.transition-colors.flex.items-center.gap-1', {
|
|
406
|
-
title: '
|
|
406
|
+
title: 'Show entire history of this record in timeline',
|
|
407
407
|
onclick: function() { focusRecord(r.resource_model, r.resource_id); },
|
|
408
408
|
}, [
|
|
409
409
|
m('span.font-semibold', r.resource_model),
|
|
@@ -425,7 +425,7 @@ function generateAuditLogComponent(options = {}) {
|
|
|
425
425
|
m('span.w-6.h-6.rounded-full.bg-slate-200.dark:bg-slate-700.text-slate-700.dark:text-slate-300.font-bold.flex.items-center.justify-center.text-[10px]',
|
|
426
426
|
((r.actor_email && r.actor_email[0]) || 'A').toUpperCase()
|
|
427
427
|
),
|
|
428
|
-
m('span.font-medium.text-gray-900.dark:text-slate-100', r.actor_email || '
|
|
428
|
+
m('span.font-medium.text-gray-900.dark:text-slate-100', r.actor_email || 'System / Automation'),
|
|
429
429
|
r.actor_id != null ? m('span.text-gray-400', '(ID: ' + r.actor_id + ')') : null,
|
|
430
430
|
]),
|
|
431
431
|
|
|
@@ -434,16 +434,16 @@ function generateAuditLogComponent(options = {}) {
|
|
|
434
434
|
|
|
435
435
|
// Changes / Mutated Fields Summary
|
|
436
436
|
r.action === 'update' && changedFields.length > 0 ? m('div.pt-1.space-y-1.5', [
|
|
437
|
-
m('span.text-xs.font-medium.text-gray-500.dark:text-slate-400', '
|
|
437
|
+
m('span.text-xs.font-medium.text-gray-500.dark:text-slate-400', 'Changed Fields:'),
|
|
438
438
|
m('div.flex.flex-wrap.gap-1.5', changedFields.map(function(f) {
|
|
439
439
|
return m('span.px-2.py-0.5.rounded-md.bg-blue-50.text-blue-700.dark:bg-blue-950/40.dark:text-blue-300.border.border-blue-200.dark:border-blue-800.text-xs.font-mono', f);
|
|
440
440
|
})),
|
|
441
441
|
]) : r.action === 'create' ? m('p.text-xs.text-gray-600.dark:text-slate-300',
|
|
442
|
-
'
|
|
442
|
+
'Record successfully created and added to database.'
|
|
443
443
|
) : r.action === 'delete' ? m('p.text-xs.text-rose-600.dark:text-rose-400',
|
|
444
|
-
'
|
|
444
|
+
'Record deleted from database (soft delete/hard delete).'
|
|
445
445
|
) : r.action === 'restore' ? m('p.text-xs.text-amber-600.dark:text-amber-400',
|
|
446
|
-
'
|
|
446
|
+
'Deleted record successfully restored.'
|
|
447
447
|
) : null,
|
|
448
448
|
|
|
449
449
|
// Details Toggle Accordion
|
|
@@ -451,17 +451,17 @@ function generateAuditLogComponent(options = {}) {
|
|
|
451
451
|
m('button.text-xs.text-indigo-600.dark:text-indigo-400.hover:underline.flex.items-center.gap-1', {
|
|
452
452
|
onclick: function() { toggleExpand(r.id); }
|
|
453
453
|
}, [
|
|
454
|
-
m('span', isExpanded ? '▲
|
|
454
|
+
m('span', isExpanded ? '▲ Hide Technical Details' : '▼ Show Technical Details'),
|
|
455
455
|
]),
|
|
456
456
|
|
|
457
457
|
isExpanded ? m('div.mt-3.p-3.rounded-lg.bg-gray-50.dark:bg-slate-900.border.border-gray-200.dark:border-slate-700.text-xs.space-y-2', [
|
|
458
458
|
m('div.grid.grid-cols-1.md:grid-cols-2.gap-2', [
|
|
459
459
|
m('div', [
|
|
460
|
-
m('span.font-semibold.text-gray-600.dark:text-slate-400', '
|
|
460
|
+
m('span.font-semibold.text-gray-600.dark:text-slate-400', 'Path: '),
|
|
461
461
|
m('span.font-mono.text-gray-800.dark:text-slate-200', (r.http_method || '') + ' ' + (r.path || '—')),
|
|
462
462
|
]),
|
|
463
463
|
m('div', [
|
|
464
|
-
m('span.font-semibold.text-gray-600.dark:text-slate-400', '
|
|
464
|
+
m('span.font-semibold.text-gray-600.dark:text-slate-400', 'User Agent: '),
|
|
465
465
|
m('span.truncate.text-gray-800.dark:text-slate-200.inline-block.max-w-xs', { title: r.user_agent || '' }, r.user_agent || '—'),
|
|
466
466
|
]),
|
|
467
467
|
]),
|
|
@@ -483,13 +483,13 @@ function generateAuditLogComponent(options = {}) {
|
|
|
483
483
|
m('table.min-w-full.text-sm', [
|
|
484
484
|
m('thead.bg-gray-50.dark:bg-slate-900.border-b.border-gray-200.dark:border-slate-700', [
|
|
485
485
|
m('tr', [
|
|
486
|
-
m('th.text-left.p-3.font-semibold.text-gray-700.dark:text-slate-300', '
|
|
487
|
-
m('th.text-left.p-3.font-semibold.text-gray-700.dark:text-slate-300', '
|
|
488
|
-
m('th.text-left.p-3.font-semibold.text-gray-700.dark:text-slate-300', '
|
|
486
|
+
m('th.text-left.p-3.font-semibold.text-gray-700.dark:text-slate-300', 'Time'),
|
|
487
|
+
m('th.text-left.p-3.font-semibold.text-gray-700.dark:text-slate-300', 'Actor'),
|
|
488
|
+
m('th.text-left.p-3.font-semibold.text-gray-700.dark:text-slate-300', 'Action'),
|
|
489
489
|
m('th.text-left.p-3.font-semibold.text-gray-700.dark:text-slate-300', 'Model'),
|
|
490
|
-
m('th.text-left.p-3.font-semibold.text-gray-700.dark:text-slate-300', '
|
|
491
|
-
m('th.text-left.p-3.font-semibold.text-gray-700.dark:text-slate-300', '
|
|
492
|
-
m('th.text-right.p-3.font-semibold.text-gray-700.dark:text-slate-300', '
|
|
490
|
+
m('th.text-left.p-3.font-semibold.text-gray-700.dark:text-slate-300', 'Record ID'),
|
|
491
|
+
m('th.text-left.p-3.font-semibold.text-gray-700.dark:text-slate-300', 'Path'),
|
|
492
|
+
m('th.text-right.p-3.font-semibold.text-gray-700.dark:text-slate-300', 'Timeline'),
|
|
493
493
|
]),
|
|
494
494
|
]),
|
|
495
495
|
m('tbody.divide-y.divide-gray-100.dark:divide-slate-700', rows.map(function(r) {
|
|
@@ -500,7 +500,7 @@ function generateAuditLogComponent(options = {}) {
|
|
|
500
500
|
m('span.text-xs.text-gray-400', r.created_at || ''),
|
|
501
501
|
]),
|
|
502
502
|
m('td.p-3.text-gray-800.dark:text-slate-200', [
|
|
503
|
-
m('span.block.font-medium', r.actor_email || '
|
|
503
|
+
m('span.block.font-medium', r.actor_email || 'System'),
|
|
504
504
|
r.actor_id != null ? m('span.text-xs.text-gray-400', '#' + r.actor_id) : null,
|
|
505
505
|
]),
|
|
506
506
|
m('td.p-3', [
|
|
@@ -514,7 +514,7 @@ function generateAuditLogComponent(options = {}) {
|
|
|
514
514
|
m('td.p-3.text-right', [
|
|
515
515
|
m('button.px-2.5.py-1.text-xs.font-medium.text-indigo-600.dark:text-indigo-400.hover:bg-indigo-50.dark:hover:bg-indigo-950/40.rounded-lg.transition-colors', {
|
|
516
516
|
onclick: function() { focusRecord(r.resource_model, r.resource_id); },
|
|
517
|
-
}, '
|
|
517
|
+
}, 'View Timeline →'),
|
|
518
518
|
]),
|
|
519
519
|
]);
|
|
520
520
|
})),
|
|
@@ -224,7 +224,7 @@ function auditLogPlugin(options = {}) {
|
|
|
224
224
|
if (registerRecordAction && typeof adminApi.registerAction === 'function') {
|
|
225
225
|
adminApi.registerAction('audit-timeline', {
|
|
226
226
|
id: 'audit-timeline',
|
|
227
|
-
label: '
|
|
227
|
+
label: 'Timeline',
|
|
228
228
|
icon: 'clock',
|
|
229
229
|
color: 'blue',
|
|
230
230
|
models: '*',
|
|
@@ -234,7 +234,7 @@ function auditLogPlugin(options = {}) {
|
|
|
234
234
|
const modelName = model?.name || (typeof model === 'string' ? model : '');
|
|
235
235
|
return {
|
|
236
236
|
redirect: `/audit-log?model=${encodeURIComponent(modelName)}&recordId=${encodeURIComponent(id)}`,
|
|
237
|
-
message: `
|
|
237
|
+
message: `Timeline: ${modelName} #${id}`,
|
|
238
238
|
};
|
|
239
239
|
},
|
|
240
240
|
});
|
|
@@ -249,54 +249,10 @@
|
|
|
249
249
|
return;
|
|
250
250
|
}
|
|
251
251
|
if (typeof window.DOMPurify !== 'undefined' && typeof window.DOMPurify.sanitize === 'function') {
|
|
252
|
-
el.innerHTML = window.DOMPurify.sanitize(htmlStr);
|
|
252
|
+
el.innerHTML = window.DOMPurify.sanitize(String(htmlStr));
|
|
253
253
|
return;
|
|
254
254
|
}
|
|
255
|
-
|
|
256
|
-
var template = document.createElement('template');
|
|
257
|
-
template.innerHTML = htmlStr;
|
|
258
|
-
var content = template.content;
|
|
259
|
-
var allowedTags = {
|
|
260
|
-
p: 1, br: 1, b: 1, i: 1, strong: 1, em: 1, u: 1, s: 1, strike: 1,
|
|
261
|
-
h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1, ul: 1, ol: 1, li: 1,
|
|
262
|
-
blockquote: 1, pre: 1, code: 1, a: 1, span: 1, div: 1, hr: 1,
|
|
263
|
-
table: 1, thead: 1, tbody: 1, tr: 1, th: 1, td: 1, img: 1
|
|
264
|
-
};
|
|
265
|
-
var allowedAttrs = {
|
|
266
|
-
href: 1, title: 1, target: 1, rel: 1, class: 1, id: 1, src: 1, alt: 1, width: 1, height: 1
|
|
267
|
-
};
|
|
268
|
-
var allNodes = content.querySelectorAll('*');
|
|
269
|
-
for (var i = allNodes.length - 1; i >= 0; i--) {
|
|
270
|
-
var node = allNodes[i];
|
|
271
|
-
var tag = node.tagName.toLowerCase();
|
|
272
|
-
if (!allowedTags[tag]) {
|
|
273
|
-
if (node.parentNode) {
|
|
274
|
-
node.parentNode.removeChild(node);
|
|
275
|
-
} else if (typeof node.remove === 'function') {
|
|
276
|
-
node.remove();
|
|
277
|
-
}
|
|
278
|
-
continue;
|
|
279
|
-
}
|
|
280
|
-
var attrs = Array.prototype.slice.call(node.attributes);
|
|
281
|
-
for (var j = 0; j < attrs.length; j++) {
|
|
282
|
-
var attrName = attrs[j].name.toLowerCase();
|
|
283
|
-
var attrVal = (attrs[j].value || '').toLowerCase().replace(/[\x00-\x20\s]+/g, '').trim();
|
|
284
|
-
if (attrName.indexOf('on') === 0 || !allowedAttrs[attrName]) {
|
|
285
|
-
node.removeAttribute(attrs[j].name);
|
|
286
|
-
} else if ((attrName === 'href' || attrName === 'src' || attrName === 'action' || attrName === 'formaction' || attrName === 'xlink:href') &&
|
|
287
|
-
/^(?:javascript|data|vbscript):/i.test(attrVal)) {
|
|
288
|
-
node.removeAttribute(attrs[j].name);
|
|
289
|
-
} else if (tag === 'a' && node.getAttribute('target') === '_blank') {
|
|
290
|
-
node.setAttribute('rel', 'noopener noreferrer');
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
el.textContent = '';
|
|
296
|
-
el.appendChild(content.cloneNode(true));
|
|
297
|
-
} catch (e) {
|
|
298
|
-
el.textContent = htmlStr;
|
|
299
|
-
}
|
|
255
|
+
el.textContent = String(htmlStr);
|
|
300
256
|
}
|
|
301
257
|
|
|
302
258
|
function updateDomFields(entryId, data) {
|
|
@@ -19,10 +19,11 @@ const ContentEntrySchema = zdb.schema({
|
|
|
19
19
|
updated_at: zdb.timestamp({ auto: 'update' }),
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
-
/**
|
|
23
|
-
* @returns {import('../../../core/orm/types').ModelDefinition}
|
|
24
|
-
*/
|
|
25
22
|
function createContentEntryModel() {
|
|
23
|
+
const { hasModel, getModel } = require('../../../core/orm/model');
|
|
24
|
+
if (hasModel('ContentEntry')) {
|
|
25
|
+
return getModel('ContentEntry');
|
|
26
|
+
}
|
|
26
27
|
return defineModel({
|
|
27
28
|
name: 'ContentEntry',
|
|
28
29
|
table: 'content_entries',
|
|
@@ -17,10 +17,11 @@ const ContentTypeSchema = zdb.schema({
|
|
|
17
17
|
updated_at: zdb.timestamp({ auto: 'update' }),
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
/**
|
|
21
|
-
* @returns {import('../../../core/orm/types').ModelDefinition}
|
|
22
|
-
*/
|
|
23
20
|
function createContentTypeModel() {
|
|
21
|
+
const { hasModel, getModel } = require('../../../core/orm/model');
|
|
22
|
+
if (hasModel('ContentType')) {
|
|
23
|
+
return getModel('ContentType');
|
|
24
|
+
}
|
|
24
25
|
return defineModel({
|
|
25
26
|
name: 'ContentType',
|
|
26
27
|
table: 'content_types',
|
package/plugins/cors.js
CHANGED
|
@@ -63,20 +63,31 @@ function createCorsMiddleware(options = {}) {
|
|
|
63
63
|
if (typeof item === 'string' && item === reqOrigin) {
|
|
64
64
|
return callback(null, reqOrigin);
|
|
65
65
|
}
|
|
66
|
-
if (item instanceof RegExp && item.test(reqOrigin)) {
|
|
66
|
+
if ((item instanceof RegExp || Object.prototype.toString.call(item) === '[object RegExp]' || item?.constructor?.name === 'RegExp') && item.test(reqOrigin)) {
|
|
67
67
|
return callback(null, reqOrigin);
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
return callback(null, false);
|
|
71
71
|
}
|
|
72
|
-
if (config.origin instanceof RegExp) {
|
|
72
|
+
if (config.origin instanceof RegExp || Object.prototype.toString.call(config.origin) === '[object RegExp]' || config.origin?.constructor?.name === 'RegExp') {
|
|
73
73
|
return callback(null, config.origin.test(reqOrigin) ? reqOrigin : false);
|
|
74
74
|
}
|
|
75
75
|
if (typeof config.origin === 'function') {
|
|
76
76
|
return config.origin(reqOrigin, callback);
|
|
77
77
|
}
|
|
78
78
|
if (config.origin === true) {
|
|
79
|
-
|
|
79
|
+
if (!reqOrigin || reqOrigin === 'null') {
|
|
80
|
+
return callback(null, false);
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const u = new URL(reqOrigin);
|
|
84
|
+
if (u.protocol !== 'http:' && u.protocol !== 'https:') {
|
|
85
|
+
return callback(null, false);
|
|
86
|
+
}
|
|
87
|
+
return callback(null, u.origin);
|
|
88
|
+
} catch {
|
|
89
|
+
return callback(null, false);
|
|
90
|
+
}
|
|
80
91
|
}
|
|
81
92
|
return callback(null, false);
|
|
82
93
|
}
|
|
@@ -101,7 +112,7 @@ function createCorsMiddleware(options = {}) {
|
|
|
101
112
|
return next(err);
|
|
102
113
|
}
|
|
103
114
|
|
|
104
|
-
if (allowedOrigin && allowedOrigin !== '*') {
|
|
115
|
+
if (allowedOrigin && allowedOrigin !== '*' && allowedOrigin !== 'null') {
|
|
105
116
|
res.setHeader('Access-Control-Allow-Origin', allowedOrigin);
|
|
106
117
|
res.setHeader('Vary', 'Origin');
|
|
107
118
|
if (config.credentials) {
|
|
@@ -26,6 +26,13 @@ const DEFAULT_AUTH_EMAILS = {
|
|
|
26
26
|
template: 'auth-welcome',
|
|
27
27
|
subject: 'Welcome',
|
|
28
28
|
},
|
|
29
|
+
magicLink: {
|
|
30
|
+
enabled: true,
|
|
31
|
+
template: 'auth-magic-link',
|
|
32
|
+
subject: 'Your sign-in link',
|
|
33
|
+
path: '/auth/magic-link/verify',
|
|
34
|
+
tokenTtlMs: 15 * 60 * 1000,
|
|
35
|
+
},
|
|
29
36
|
registerRoutes: true,
|
|
30
37
|
routePrefix: '/api/auth',
|
|
31
38
|
};
|
|
@@ -46,6 +53,7 @@ function wireAuthEmails(authManager, emailService, registry, config, db) {
|
|
|
46
53
|
passwordReset: { ...DEFAULT_AUTH_EMAILS.passwordReset, ...config.passwordReset },
|
|
47
54
|
emailVerification: { ...DEFAULT_AUTH_EMAILS.emailVerification, ...config.emailVerification },
|
|
48
55
|
welcome: { ...DEFAULT_AUTH_EMAILS.welcome, ...config.welcome },
|
|
56
|
+
magicLink: { ...DEFAULT_AUTH_EMAILS.magicLink, ...config.magicLink },
|
|
49
57
|
};
|
|
50
58
|
|
|
51
59
|
registerBundledAuthTemplates(registry);
|
|
@@ -111,6 +119,23 @@ function wireAuthEmails(authManager, emailService, registry, config, db) {
|
|
|
111
119
|
};
|
|
112
120
|
}
|
|
113
121
|
|
|
122
|
+
if (merged.magicLink?.enabled) {
|
|
123
|
+
authManager.notifications.magicLink = async (user, { token, magicLinkUrl, expiresMinutes }) => {
|
|
124
|
+
const url = magicLinkUrl || buildUrl(merged.magicLink.path, token);
|
|
125
|
+
await emailService.sendTemplate(merged.magicLink.template, {
|
|
126
|
+
to: user[identifierField],
|
|
127
|
+
subject: merged.magicLink.subject,
|
|
128
|
+
data: {
|
|
129
|
+
name: userDisplayName(user),
|
|
130
|
+
token,
|
|
131
|
+
magicLinkUrl: url,
|
|
132
|
+
expiresMinutes: expiresMinutes || 15,
|
|
133
|
+
email: user[identifierField],
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
114
139
|
return merged;
|
|
115
140
|
}
|
|
116
141
|
|
|
@@ -193,6 +218,41 @@ function registerAuthEmailRoutes(options) {
|
|
|
193
218
|
}
|
|
194
219
|
}
|
|
195
220
|
|
|
221
|
+
async function magicLinkRequestHandler(req, res) {
|
|
222
|
+
try {
|
|
223
|
+
const email = req.body?.email || req.body?.identifier;
|
|
224
|
+
if (email) {
|
|
225
|
+
await authManager.requestMagicLink(String(email), {
|
|
226
|
+
tokenTtlMs: authEmailsConfig.magicLink?.tokenTtlMs,
|
|
227
|
+
path: authEmailsConfig.magicLink?.path,
|
|
228
|
+
buildMagicLinkUrl: (token) => {
|
|
229
|
+
const pathSeg = authEmailsConfig.magicLink?.path || '/auth/magic-link/verify';
|
|
230
|
+
return `${base}${pathSeg}?token=${encodeURIComponent(token)}`;
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
res.json({ ok: true, message: 'If the account exists, a sign-in link has been sent.' });
|
|
235
|
+
} catch (err) {
|
|
236
|
+
res.status(500).json({ error: err.message });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function magicLinkVerifyHandler(req, res) {
|
|
241
|
+
try {
|
|
242
|
+
const token = req.body?.token || req.query?.token;
|
|
243
|
+
if (!token) {
|
|
244
|
+
return res.status(400).json({ error: 'token is required' });
|
|
245
|
+
}
|
|
246
|
+
const { user } = await authManager.verifyMagicLink(String(token));
|
|
247
|
+
if (req.auth && typeof req.auth.login === 'function') {
|
|
248
|
+
await req.auth.login(user);
|
|
249
|
+
}
|
|
250
|
+
res.json({ ok: true, user });
|
|
251
|
+
} catch (err) {
|
|
252
|
+
res.status(err.status || 400).json({ error: err.message, code: err.code });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
196
256
|
const forgotMw = guest ? [guest, forgotPasswordHandler] : [forgotPasswordHandler];
|
|
197
257
|
const resetMw = guest ? [guest, resetPasswordHandler] : [resetPasswordHandler];
|
|
198
258
|
const resendMw = authMw ? [authMw, resendVerificationHandler] : [resendVerificationHandler];
|
|
@@ -201,6 +261,12 @@ function registerAuthEmailRoutes(options) {
|
|
|
201
261
|
ctx.addRoute('post', `${prefix}/reset-password`, ...resetMw);
|
|
202
262
|
ctx.addRoute('post', `${prefix}/verify-email`, verifyEmailHandler);
|
|
203
263
|
ctx.addRoute('post', `${prefix}/resend-verification`, ...resendMw);
|
|
264
|
+
|
|
265
|
+
if (authEmailsConfig.magicLink?.enabled) {
|
|
266
|
+
const magicMw = guest ? [guest, magicLinkRequestHandler] : [magicLinkRequestHandler];
|
|
267
|
+
ctx.addRoute('post', `${prefix}/magic-link`, ...magicMw);
|
|
268
|
+
ctx.addRoute('post', `${prefix}/magic-link/verify`, magicLinkVerifyHandler);
|
|
269
|
+
}
|
|
204
270
|
}
|
|
205
271
|
|
|
206
272
|
module.exports = {
|
|
@@ -47,6 +47,21 @@ const BUNDLED_AUTH_TEMPLATES = {
|
|
|
47
47
|
</mj-column>
|
|
48
48
|
</mj-section>
|
|
49
49
|
</mj-body>
|
|
50
|
+
</mjml>`,
|
|
51
|
+
},
|
|
52
|
+
'auth-magic-link': {
|
|
53
|
+
mjml: `<mjml>
|
|
54
|
+
<mj-body background-color="#f4f4f5">
|
|
55
|
+
<mj-section background-color="#ffffff" padding="32px 24px">
|
|
56
|
+
<mj-column>
|
|
57
|
+
<mj-text font-size="20px" font-weight="600" color="#18181b">Your sign-in link</mj-text>
|
|
58
|
+
<mj-text color="#52525b">Hi {{name}},</mj-text>
|
|
59
|
+
<mj-text color="#52525b">Click the button below to securely sign in to your account. This link will expire in {{expiresMinutes}} minutes.</mj-text>
|
|
60
|
+
<mj-button background-color="#4f46e5" href="{{magicLinkUrl}}">Sign In</mj-button>
|
|
61
|
+
<mj-text color="#71717a" font-size="12px">If you did not request this link, you can safely ignore this email.</mj-text>
|
|
62
|
+
</mj-column>
|
|
63
|
+
</mj-section>
|
|
64
|
+
</mj-body>
|
|
50
65
|
</mjml>`,
|
|
51
66
|
},
|
|
52
67
|
};
|