millas 0.2.13 → 0.2.15

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 (88) hide show
  1. package/package.json +6 -3
  2. package/src/admin/Admin.js +107 -1027
  3. package/src/admin/AdminAuth.js +1 -1
  4. package/src/admin/ViewContext.js +1 -1
  5. package/src/admin/handlers/ActionHandler.js +103 -0
  6. package/src/admin/handlers/ApiHandler.js +113 -0
  7. package/src/admin/handlers/AuthHandler.js +76 -0
  8. package/src/admin/handlers/ExportHandler.js +70 -0
  9. package/src/admin/handlers/InlineHandler.js +71 -0
  10. package/src/admin/handlers/PageHandler.js +351 -0
  11. package/src/admin/resources/AdminResource.js +22 -1
  12. package/src/admin/static/SelectFilter2.js +34 -0
  13. package/src/admin/static/actions.js +201 -0
  14. package/src/admin/static/admin.css +7 -0
  15. package/src/admin/static/change_form.js +585 -0
  16. package/src/admin/static/core.js +128 -0
  17. package/src/admin/static/login.js +76 -0
  18. package/src/admin/static/vendor/bi/bootstrap-icons.min.css +5 -0
  19. package/src/admin/static/vendor/bi/fonts/bootstrap-icons.woff +0 -0
  20. package/src/admin/static/vendor/bi/fonts/bootstrap-icons.woff2 +0 -0
  21. package/src/admin/static/vendor/jquery.min.js +2 -0
  22. package/src/admin/views/layouts/base.njk +30 -113
  23. package/src/admin/views/pages/detail.njk +10 -9
  24. package/src/admin/views/pages/form.njk +4 -4
  25. package/src/admin/views/pages/list.njk +11 -193
  26. package/src/admin/views/pages/login.njk +19 -64
  27. package/src/admin/views/partials/form-field.njk +1 -1
  28. package/src/admin/views/partials/form-scripts.njk +4 -478
  29. package/src/admin/views/partials/form-widget.njk +10 -10
  30. package/src/ai/AITokenBudget.js +1 -1
  31. package/src/auth/Auth.js +112 -3
  32. package/src/auth/AuthMiddleware.js +18 -15
  33. package/src/auth/Hasher.js +15 -43
  34. package/src/cli.js +3 -0
  35. package/src/commands/call.js +190 -0
  36. package/src/commands/createsuperuser.js +3 -4
  37. package/src/commands/key.js +97 -0
  38. package/src/commands/make.js +16 -2
  39. package/src/commands/new.js +16 -1
  40. package/src/commands/serve.js +5 -5
  41. package/src/console/Command.js +337 -0
  42. package/src/console/CommandLoader.js +165 -0
  43. package/src/console/index.js +6 -0
  44. package/src/container/AppInitializer.js +48 -1
  45. package/src/container/Application.js +3 -1
  46. package/src/container/HttpServer.js +0 -1
  47. package/src/container/MillasConfig.js +48 -0
  48. package/src/controller/Controller.js +13 -11
  49. package/src/core/docs.js +6 -0
  50. package/src/core/foundation.js +8 -0
  51. package/src/core/http.js +20 -10
  52. package/src/core/validation.js +58 -27
  53. package/src/docs/Docs.js +268 -0
  54. package/src/docs/DocsServiceProvider.js +80 -0
  55. package/src/docs/SchemaInferrer.js +131 -0
  56. package/src/docs/handlers/ApiHandler.js +305 -0
  57. package/src/docs/handlers/PageHandler.js +47 -0
  58. package/src/docs/index.js +13 -0
  59. package/src/docs/resources/ApiResource.js +402 -0
  60. package/src/docs/static/docs.css +723 -0
  61. package/src/docs/static/docs.js +1181 -0
  62. package/src/encryption/Encrypter.js +381 -0
  63. package/src/facades/Auth.js +5 -2
  64. package/src/facades/Crypt.js +166 -0
  65. package/src/facades/Docs.js +43 -0
  66. package/src/facades/Mail.js +1 -1
  67. package/src/http/MillasRequest.js +7 -31
  68. package/src/http/RequestContext.js +11 -7
  69. package/src/http/SecurityBootstrap.js +24 -2
  70. package/src/http/Shape.js +168 -0
  71. package/src/http/adapters/ExpressAdapter.js +9 -5
  72. package/src/middleware/CorsMiddleware.js +3 -0
  73. package/src/middleware/ThrottleMiddleware.js +10 -7
  74. package/src/orm/model/Model.js +20 -2
  75. package/src/providers/EncryptionServiceProvider.js +66 -0
  76. package/src/router/MiddlewareRegistry.js +79 -54
  77. package/src/router/Route.js +9 -4
  78. package/src/router/RouteEntry.js +91 -0
  79. package/src/router/Router.js +71 -1
  80. package/src/scaffold/maker.js +138 -1
  81. package/src/scaffold/templates.js +12 -0
  82. package/src/serializer/Serializer.js +239 -0
  83. package/src/support/Str.js +1080 -0
  84. package/src/validation/BaseValidator.js +45 -5
  85. package/src/validation/Validator.js +67 -61
  86. package/src/validation/types.js +490 -0
  87. package/src/middleware/AuthMiddleware.js +0 -46
  88. package/src/middleware/MiddlewareRegistry.js +0 -106
@@ -65,7 +65,7 @@
65
65
 
66
66
  <div class="toolbar-right">
67
67
  {% if filters | length %}
68
- <button class="btn btn-ghost btn-sm" onclick="toggleFilters()" id="filter-toggle">
68
+ <button class="btn btn-ghost btn-sm" id="filter-toggle">
69
69
  <span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-filter"/></svg></span>
70
70
  Filters
71
71
  {% if activeFilters | length %}
@@ -100,18 +100,18 @@
100
100
  <span class="bulk-count" id="bulk-count">0 selected</span>
101
101
  <span class="text-muted" style="font-size:12px">—</span>
102
102
  {% for action in resource.actions %}
103
- <button class="btn btn-ghost btn-sm" onclick="bulkAction({{ action.index }})">
103
+ <button class="btn btn-ghost btn-sm" data-bulk-action="{{ action.index }}">
104
104
  {% if action.icon %}<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-{{ action.icon }}"/></svg></span>{% endif %}
105
105
  {{ action.label }}
106
106
  </button>
107
107
  {% endfor %}
108
108
  {% if resource.canDelete %}
109
- <button class="btn btn-ghost btn-sm btn-danger" onclick="bulkDelete()">
109
+ <button class="btn btn-ghost btn-sm btn-danger" id="bulk-delete-btn">
110
110
  <span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-trash"/></svg></span>
111
111
  Delete selected
112
112
  </button>
113
113
  {% endif %}
114
- <button class="btn btn-ghost btn-sm" style="margin-left:auto" onclick="clearSelection()">
114
+ <button class="btn btn-ghost btn-sm" style="margin-left:auto" id="clear-selection">
115
115
  <span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-x"/></svg></span>
116
116
  Cancel
117
117
  </button>
@@ -171,7 +171,7 @@
171
171
  <thead>
172
172
  <tr>
173
173
  <th class="col-check">
174
- <input type="checkbox" class="row-check" id="check-all" onchange="toggleAll(this)" title="Select all">
174
+ <input type="checkbox" class="row-check" id="check-all" title="Select all">
175
175
  </th>
176
176
  {% for field in listFields %}
177
177
  {% set isSorted = sort == field.name %}
@@ -323,196 +323,14 @@
323
323
  </div>
324
324
  </div>
325
325
 
326
+ {% block scripts %}
326
327
  <script>
327
- $(function() {
328
- var PREFIX = '{{ adminPrefix }}';
329
- var SLUG = '{{ resource.slug }}';
330
-
331
- // ── Filter panel ─────────────────────────────────────────────────────────
332
- {% if activeFilters | length %}$('#filter-panel').show();{% endif %}
333
-
334
- window.toggleFilters = function() {
335
- $('#filter-panel').toggle();
336
- };
337
-
338
- // ── Live search on Enter ──────────────────────────────────────────────────
339
- $('#search-form input[name="search"]').on('keydown', function(e) {
340
- if (e.key === 'Enter') $('#search-form').submit();
341
- });
342
-
343
- // ── Bulk selection ────────────────────────────────────────────────────────
344
- function updateBulkBar() {
345
- var $checked = $('.item-check:checked');
346
- var n = $checked.length;
347
- var total = $('.item-check').length;
348
- $('#bulk-bar').toggleClass('visible', n > 0);
349
- $('#bulk-count').text(n + ' selected');
350
- $('#check-all').prop('indeterminate', n > 0 && n < total)
351
- .prop('checked', n > 0 && n === total);
352
- }
353
-
354
- window.toggleAll = function(master) {
355
- $('.item-check').prop('checked', master.checked);
356
- updateBulkBar();
357
- };
358
-
359
- window.clearSelection = function() {
360
- $('.item-check, #check-all').prop('checked', false).prop('indeterminate', false);
361
- updateBulkBar();
362
- };
363
-
364
- $(document).on('change', '.item-check', updateBulkBar);
365
-
366
- window.bulkDelete = function() {
367
- var ids = $('.item-check:checked').map(function() { return this.value; }).get();
368
- if (!ids.length) return;
369
- var label = ids.length + ' record' + (ids.length > 1 ? 's' : '');
370
- confirmDeleteBulk(PREFIX + '/' + SLUG + '/bulk-delete', label, ids);
371
- };
372
-
373
- window.confirmDeleteBulk = function(url, label, ids) {
374
- UI.Confirm.show({
375
- title: 'Delete ' + label,
376
- message: 'Delete <strong>' + label + '</strong>? This cannot be undone.',
377
- confirm: 'Delete',
378
- danger: true,
379
- }).then(function(ok) {
380
- if (!ok) return;
381
- var csrf = $('meta[name="csrf-token"]').attr('content') || '';
382
- var $form = $('<form method="POST">').attr('action', url);
383
- $form.append('<input name="_csrf" value="' + csrf + '">');
384
- $.each(ids, function(_, id) {
385
- $form.append('<input type="hidden" name="ids[]" value="' + id + '">');
386
- });
387
- $form.appendTo('body').submit();
388
- });
389
- };
390
-
391
- window.bulkAction = function(actionIndex) {
392
- var ids = $('.item-check:checked').map(function() { return this.value; }).get();
393
- if (!ids.length) return;
394
- var csrf = $('meta[name="csrf-token"]').attr('content') || '';
395
- var $form = $('<form method="POST">').attr('action', PREFIX + '/' + SLUG + '/bulk-action');
396
- $form.append('<input name="_csrf" value="' + csrf + '">');
397
- $form.append('<input type="hidden" name="actionIndex" value="' + actionIndex + '">');
398
- $.each(ids, function(_, id) {
399
- $form.append('<input type="hidden" name="ids[]" value="' + id + '">');
400
- });
401
- $form.appendTo('body').submit();
402
- };
403
-
404
- // ── UI.Dropdown — portal-rendered action menus ────────────────────────────
405
- // Wire export menu
406
- var $exportBtn = $('#export-menu-btn');
407
- var $exportPanel = $('#export-menu-panel');
408
- if ($exportBtn.length && $exportPanel.length) {
409
- var exportDd = UI.Dropdown.create({
410
- anchor: $exportBtn[0],
411
- content: $exportPanel[0],
412
- placement: 'bottom-end',
413
- offset: 4,
414
- });
415
- $exportBtn.on('click', function() { exportDd.toggle(); });
416
- }
417
-
418
- // Wire per-row action menus
419
- $('.ui-menu').each(function() {
420
- var $menu = $(this);
421
- var $btn = $menu.find('.ui-menu-trigger');
422
- var $panel = $menu.find('.ui-menu-panel');
423
- if (!$btn.length || !$panel.length) return;
424
-
425
- var dd = UI.Dropdown.create({
426
- anchor: $btn[0],
427
- content: $panel[0],
428
- placement: 'bottom-end',
429
- offset: 4,
430
- });
431
-
432
- $btn.on('click', function(e) {
433
- e.stopPropagation();
434
- dd.toggle();
435
- });
436
-
437
- $panel.find('[data-confirm-delete]').on('click', function() {
438
- dd.close();
439
- confirmDelete($(this).data('confirm-delete'), $(this).data('confirm-label'));
440
- });
441
-
442
- $panel.find('[data-row-action]').on('click', function() {
443
- dd.close();
444
- var url = $(this).data('row-action');
445
- var label = $(this).data('row-action-label');
446
- UI.Confirm.show({
447
- title: label,
448
- message: 'Run <strong>' + label + '</strong> on this record?',
449
- confirm: label,
450
- }).then(function(ok) {
451
- if (!ok) return;
452
- var csrf = $('meta[name="csrf-token"]').attr('content') || '';
453
- var $form = $('<form method="POST">').attr('action', url);
454
- $form.append('<input name="_csrf" value="' + csrf + '">');
455
- $form.appendTo('body').submit();
456
- });
457
- });
458
-
459
- // ── Right-click on the row opens a context menu at the cursor ──────────
460
- (function($menuPanel) {
461
- var $row = $menuPanel.closest('tr');
462
-
463
- $row.on('contextmenu', function(e) {
464
- e.preventDefault();
465
- e.stopPropagation();
466
-
467
- // Remove any existing portal
468
- $('.context-menu-portal').remove();
469
-
470
- // Build a fresh portal from the panel's inner HTML (avoids clone display:none)
471
- var $p = $('<div class="ui-menu-panel context-menu-portal"></div>');
472
- $p.html($menuPanel.html());
473
- $p.css({
474
- position: 'fixed',
475
- top: Math.min(e.clientY, window.innerHeight - 240) + 'px',
476
- left: Math.min(e.clientX, window.innerWidth - 190) + 'px',
477
- zIndex: 9999,
478
- });
479
- $('body').append($p);
480
-
481
- // Wire handlers on the portal
482
- $p.find('[data-confirm-delete]').on('click', function() {
483
- $p.remove();
484
- confirmDelete($(this).data('confirm-delete'), $(this).data('confirm-label'));
485
- });
486
- $p.find('[data-row-action]').on('click', function() {
487
- $p.remove();
488
- var _url = $(this).data('row-action');
489
- var _label = $(this).data('row-action-label');
490
- UI.Confirm.show({
491
- title: _label,
492
- message: 'Run <strong>' + _label + '</strong> on this record?',
493
- confirm: _label,
494
- }).then(function(ok) {
495
- if (!ok) return;
496
- var csrf = $('meta[name="csrf-token"]').attr('content') || '';
497
- var $form = $('<form method="POST">').attr('action', _url);
498
- $form.append('<input name="_csrf" value="' + csrf + '">');
499
- $form.appendTo('body').submit();
500
- });
501
- });
502
- $p.find('a.ui-menu-item').on('click', function() { $p.remove(); });
503
-
504
- // Dismiss on next click anywhere or Escape
505
- setTimeout(function() {
506
- $(document).one('click.ctxmenu', function() { $p.remove(); });
507
- }, 0);
508
- $(document).one('keydown.ctxmenu', function(ev) {
509
- if (ev.key === 'Escape') { $p.remove(); }
510
- });
511
- });
512
- })($panel);
513
- });
514
- });
328
+ window.MILLAS_ADMIN_PREFIX = '{{ adminPrefix }}';
329
+ window.MILLAS_RESOURCE_SLUG = '{{ resource.slug }}';
330
+ window.MILLAS_HAS_ACTIVE_FILTERS = {% if activeFilters | length %}true{% else %}false{% endif %};
515
331
  </script>
332
+ <script src="{{ adminPrefix }}/static/actions.js?v=2"></script>
333
+ {% endblock %}
516
334
 
517
335
  <style>
518
336
  /* ── UI Menu (portal dropdown) ── */
@@ -4,22 +4,19 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Sign In — {{ adminTitle }}</title>
7
- <link rel="preconnect" href="https://fonts.googleapis.com">
8
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
- <link href="https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,300;9..40,400;9..40,500;9..40,600;9..40,700&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
10
7
  <style>
11
8
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
12
9
  :root {
13
- --bg: #f0f2f5;
10
+ --bg: #f6f7f8;
14
11
  --surface: #ffffff;
15
12
  --border: #e3e6ec;
16
13
  --border-soft: #edf0f5;
17
- --primary: #2563eb;
18
- --primary-h: #1d4ed8;
19
- --primary-soft: #eff4ff;
20
- --primary-dim: #dbeafe;
21
- --text: #111827;
22
- --text-soft: #374151;
14
+ --primary: #f6821f;
15
+ --primary-h: #e06d1a;
16
+ --primary-soft: #fff4e6;
17
+ --primary-dim: #ffe4c4;
18
+ --text: #172b4d;
19
+ --text-soft: #2c3e50;
23
20
  --text-muted: #6b7280;
24
21
  --text-xmuted: #9ca3af;
25
22
  --danger: #dc2626;
@@ -51,7 +48,7 @@
51
48
  .login-card {
52
49
  background: var(--surface);
53
50
  border: 1px solid var(--border);
54
- border-radius: 14px;
51
+ border-radius: 6px;
55
52
  box-shadow: var(--shadow);
56
53
  width: 100%;
57
54
  max-width: 400px;
@@ -203,6 +200,15 @@
203
200
  radial-gradient(ellipse 60% 40% at 80% 90%, rgba(99,102,241,.05) 0%, transparent 60%);
204
201
  pointer-events: none;
205
202
  }
203
+ input:-webkit-autofill,
204
+ input:-webkit-autofill:hover,
205
+ input:-webkit-autofill:focus,
206
+ input:-webkit-autofill:active {
207
+ -webkit-box-shadow: 0 0 0 100px white inset !important;
208
+ -webkit-text-fill-color: #000 !important;
209
+ caret-color: #000;
210
+ transition: background-color 5000s ease-in-out 0s;
211
+ }
206
212
  </style>
207
213
  </head>
208
214
  <body>
@@ -210,13 +216,6 @@
210
216
  <div class="login-card">
211
217
 
212
218
  <div class="login-header">
213
- <div class="login-logo">
214
- <svg viewBox="0 0 24 24">
215
- <ellipse cx="12" cy="5" rx="9" ry="3"/>
216
- <path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/>
217
- <path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/>
218
- </svg>
219
- </div>
220
219
  <div class="login-title">{{ adminTitle }}</div>
221
220
  <div class="login-subtitle">Sign in to your admin panel</div>
222
221
  </div>
@@ -270,7 +269,7 @@
270
269
  autocomplete="current-password"
271
270
  required
272
271
  style="padding-right: 40px">
273
- <button type="button" class="pw-toggle" onclick="togglePw()" aria-label="Toggle password visibility">
272
+ <button type="button" class="pw-toggle" aria-label="Toggle password visibility">
274
273
  <svg id="pw-icon" viewBox="0 0 24 24">
275
274
  <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
276
275
  <circle cx="12" cy="12" r="3"/>
@@ -304,51 +303,7 @@
304
303
 
305
304
  </div>
306
305
 
307
- <script>
308
- function togglePw() {
309
- const input = document.getElementById('password');
310
- const icon = document.getElementById('pw-icon');
311
- const isHidden = input.type === 'password';
312
- input.type = isHidden ? 'text' : 'password';
313
- icon.innerHTML = isHidden
314
- ? `<path d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94"/><path d="M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19"/><line x1="1" y1="1" x2="23" y2="23"/>`
315
- : `<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>`;
316
- }
317
-
318
- // Submit loading state + basic client validation
319
- document.getElementById('login-form').addEventListener('submit', function(e) {
320
- const email = document.getElementById('email').value.trim();
321
- const password = document.getElementById('password').value;
322
-
323
- if (!email || !password) {
324
- e.preventDefault();
325
- const existing = document.getElementById('login-error');
326
- const msg = !email ? 'Email is required.' : 'Password is required.';
327
- if (existing) {
328
- existing.querySelector('svg + *')
329
- ? (existing.lastChild.textContent = msg)
330
- : (existing.innerHTML += ` ${msg}`);
331
- } else {
332
- const el = document.createElement('div');
333
- el.className = 'alert alert-error';
334
- el.id = 'login-error';
335
- el.innerHTML = `<svg viewBox="0 0 24 24" width="15" height="15"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg> ${msg}`;
336
- this.insertBefore(el, this.firstChild);
337
- }
338
- return;
339
- }
340
-
341
- const btn = document.getElementById('login-btn');
342
- const icon = document.getElementById('login-icon');
343
- btn.disabled = true;
344
- icon.innerHTML = `<circle cx="12" cy="12" r="10" stroke-dasharray="32" stroke-dashoffset="32" class="spin"/>`;
345
- btn.lastChild.textContent = ' Signing in…';
346
- });
347
-
348
- // Auto-focus password if email is pre-filled
349
- const emailVal = document.getElementById('email').value;
350
- if (emailVal) document.getElementById('password').focus();
351
- </script>
306
+ <script src="{{ adminPrefix }}/static/login.js?v=1"></script>
352
307
 
353
308
  </body>
354
309
  </html>
@@ -42,7 +42,7 @@
42
42
  {% if hasError %}
43
43
  <span class="form-error">
44
44
  <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
45
- {{ errors[field.name][0] if errors[field.name] is iterable else errors[field.name] }}
45
+ {{ errors[field.name] if errors[field.name] is string else errors[field.name][0] }}
46
46
  </span>
47
47
  {% elif field.help %}
48
48
  <span class="form-help">{{ field.help }}</span>