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.
Files changed (108) hide show
  1. package/.agents/ADMIN.md +47 -4
  2. package/.agents/AGENTS.md +4 -2
  3. package/.agents/AUTH.md +309 -0
  4. package/.agents/CLI.md +128 -0
  5. package/.agents/EMAIL.md +84 -2
  6. package/.agents/ERRORS.md +59 -0
  7. package/.agents/EVENTS.md +179 -0
  8. package/.agents/ORM.md +149 -0
  9. package/.agents/PLUGINS.md +44 -7
  10. package/.agents/QUEUE.md +18 -10
  11. package/.agents/ROUTING.md +97 -1
  12. package/.agents/STREAMING.md +35 -0
  13. package/AGENTS.md +2 -0
  14. package/README.md +145 -93
  15. package/bin/commands/add-docker.js +467 -0
  16. package/bin/commands/db-makemigrations.js +83 -0
  17. package/bin/commands/deploy-check.js +339 -0
  18. package/bin/commands/list.js +96 -0
  19. package/bin/commands/make-command.js +93 -0
  20. package/bin/commands/make-event.js +80 -0
  21. package/bin/commands/make-factory.js +72 -0
  22. package/bin/commands/make-mail.js +93 -0
  23. package/bin/commands/make-policy.js +106 -0
  24. package/bin/commands/run.js +195 -0
  25. package/bin/webspresso.js +21 -0
  26. package/core/auth/bouncer.js +196 -0
  27. package/core/auth/index.js +73 -1
  28. package/core/auth/magic-link.js +170 -0
  29. package/core/auth/manager.js +343 -3
  30. package/core/auth/middleware.js +45 -6
  31. package/core/auth/session-manager.js +255 -0
  32. package/core/auth/sessions.js +351 -0
  33. package/core/auth/tokens.js +1 -0
  34. package/core/auth/totp.js +4 -1
  35. package/core/auth/user-agent.js +161 -0
  36. package/core/commands/define-command.js +48 -0
  37. package/core/commands/index.js +18 -0
  38. package/core/commands/registry.js +156 -0
  39. package/core/commands/runner.js +76 -0
  40. package/core/compression/compressible.js +1 -1
  41. package/core/compression/middleware.js +35 -251
  42. package/core/content/service.js +8 -2
  43. package/core/events/discovery.js +142 -0
  44. package/core/events/dispatcher.js +552 -0
  45. package/core/events/index.js +23 -0
  46. package/core/events/signal.js +92 -0
  47. package/core/forms/flash.js +234 -0
  48. package/core/forms/form-builder.js +214 -0
  49. package/core/forms/index.js +16 -0
  50. package/core/mail/index.js +23 -0
  51. package/core/mail/mailable.js +250 -0
  52. package/core/mail/mailer.js +184 -0
  53. package/core/mail/transports.js +238 -0
  54. package/core/orm/factory.js +250 -0
  55. package/core/orm/index.js +8 -1
  56. package/core/orm/migrations/diff.js +185 -0
  57. package/core/orm/migrations/index.js +22 -4
  58. package/core/orm/model.js +55 -1
  59. package/core/orm/repository.js +283 -13
  60. package/core/orm/scopes.js +3 -2
  61. package/core/orm/types.js +6 -1
  62. package/core/queue/cron.js +252 -39
  63. package/core/queue/manager.js +20 -2
  64. package/core/queue/scheduler.js +115 -59
  65. package/core/ssr/stream.js +62 -2
  66. package/index.d.ts +540 -14
  67. package/index.js +44 -0
  68. package/package.json +11 -5
  69. package/plugins/admin-panel/api.js +509 -3
  70. package/plugins/admin-panel/index.js +17 -0
  71. package/plugins/admin-panel/modules/admin-users.js +1 -1
  72. package/plugins/audit-log/admin-component.js +54 -54
  73. package/plugins/audit-log/index.js +2 -2
  74. package/plugins/content/client/inline-edit.js +2 -46
  75. package/plugins/content/models/content-entry.js +4 -3
  76. package/plugins/content/models/content-type.js +4 -3
  77. package/plugins/cors.js +15 -4
  78. package/plugins/email/auth-bridge.js +66 -0
  79. package/plugins/email/bundled-templates.js +15 -0
  80. package/plugins/email/index.js +48 -0
  81. package/plugins/email/preview-handler.js +555 -0
  82. package/plugins/email/service.js +23 -12
  83. package/plugins/email/templates/auth-magic-link.mjml +13 -0
  84. package/plugins/email/transport.js +30 -11
  85. package/plugins/file-manager/admin-component.js +49 -49
  86. package/plugins/file-manager/index.js +4 -4
  87. package/plugins/index.js +2 -0
  88. package/plugins/redirect/index.js +1 -1
  89. package/plugins/webhooks/admin-component.js +608 -0
  90. package/plugins/webhooks/api-handlers.js +237 -0
  91. package/plugins/webhooks/crypto.js +144 -0
  92. package/plugins/webhooks/dispatcher.js +708 -0
  93. package/plugins/webhooks/index.js +263 -0
  94. package/plugins/webhooks/matcher.js +113 -0
  95. package/plugins/webhooks/migration-template.js +75 -0
  96. package/plugins/webhooks/models.js +98 -0
  97. package/plugins/webhooks/sender.js +176 -0
  98. package/src/api/api-loader.js +1 -1
  99. package/src/app-context.js +48 -6
  100. package/src/bundle/index.js +2 -1
  101. package/src/dev-reload/index.js +9 -1
  102. package/src/discovery/scan-routes.js +10 -1
  103. package/src/file-router.js +173 -4
  104. package/src/helpers.js +150 -1
  105. package/src/njk-frontmatter.js +3 -0
  106. package/src/pages/page-loader.js +1 -1
  107. package/src/routing/cascading.js +120 -4
  108. 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. Ahmet Yılmaz',
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 (Zaman Tüneli) with record focus & diff metadata
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 'Az önce';
30
- if (diffMin < 60) return diffMin + ' dk önce';
31
- if (diffHour < 24) return diffHour + ' sa önce';
32
- if (diffDay === 1) return 'Dün';
33
- if (diffDay < 30) return diffDay + ' gün önce';
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: 'OLUŞTURULDU',
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: 'GÜNCELLENDİ',
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: 'SİLİNDİ',
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: 'GERİ YÜKLENDİ',
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 || 'İŞLEM').toUpperCase(),
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', 'Denetim Günlüğü & Zaman Tüneli'),
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
- 'Tüm yönetici CRUD işlemlerinin ve model mutasyonlarının denetim geçmişi.'
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', 'Zaman Tüneli'),
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', 'Tablo'),
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('90 günden eski denetim günlükleri silinsin mi?')) return;
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('Temizleme hatası: ' + (e.message || String(e)));
265
+ alert('Purge error: ' + (e.message || String(e)));
266
266
  });
267
267
  },
268
- }, '90 Gün Öncesini Temizle'),
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 + ' #' : 'Kayıt #') + filterRecordId + ' İçin Değişiklik Zaman Tüneli'
278
+ 'Change Timeline for ' + (filterModel ? filterModel + ' #' : 'Record #') + filterRecordId
279
279
  ),
280
280
  m('p.text-xs.text-indigo-700.dark:text-indigo-400',
281
- 'Bu kayda ait tüm ekleme, güncelleme ve silme geçmişi filtreleniyor.'
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
- }, '✕ Filtreyi Kaldır'),
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: 'Örn. Post, User',
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', 'Kayıt ID'),
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: 'Örn. 42',
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', 'İşlem Türü'),
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: '' }, 'Tüm İşlemler'),
323
- m('option', { value: 'create' }, 'Oluşturma (create)'),
324
- m('option', { value: 'update' }, 'Güncelleme (update)'),
325
- m('option', { value: 'delete' }, 'Silme (delete)'),
326
- m('option', { value: 'restore' }, 'Geri Yükleme (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', 'Arama'),
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: 'E-posta, IP veya yol...',
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', 'Denetim kayıtları yükleniyor...'),
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', 'Kayıtlar Yüklenemedi'),
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', 'Kayıt Bulunamadı'),
372
- m('p.text-sm.text-gray-500.dark:text-slate-400.mt-1', 'Seçilen filtrelere uygun herhangi bir denetim kaydı bulunamadı.'),
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
- }, 'Filtreleri temizle ve tekrar dene') : null,
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: 'Bu kaydın tüm geçmişini zaman tünelinde göster',
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 || 'Sistem / Otomasyon'),
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', 'Değiştirilen Alanlar:'),
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
- 'Kayıt başarıyla oluşturuldu ve veritabanına eklendi.'
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
- 'Kayıt veritabanından silindi (soft delete/hard delete).'
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
- 'Silinmiş kayıt başarıyla geri yüklendi.'
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 ? '▲ Teknik Detayları Gizle' : '▼ Teknik Detayları Göster'),
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', 'Yol: '),
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', 'Tarayıcı / Agent: '),
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', 'Zaman'),
487
- m('th.text-left.p-3.font-semibold.text-gray-700.dark:text-slate-300', 'Kullanıcı'),
488
- m('th.text-left.p-3.font-semibold.text-gray-700.dark:text-slate-300', 'İşlem'),
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', 'Kayıt ID'),
491
- m('th.text-left.p-3.font-semibold.text-gray-700.dark:text-slate-300', 'Yol'),
492
- m('th.text-right.p-3.font-semibold.text-gray-700.dark:text-slate-300', 'Zaman Tüneli'),
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 || 'Sistem'),
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
- }, 'Tünelde Gör →'),
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: 'Zaman Tüneli',
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: `Zaman tüneli: ${modelName} #${id}`,
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
- try {
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
- return callback(null, reqOrigin);
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
  };