neoagent 2.4.1-beta.45 → 2.4.1-beta.46
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/.env.example +12 -0
- package/package.json +1 -1
- package/server/admin/access.js +198 -0
- package/server/admin/admin.js +1 -1
- package/server/admin/analytics.js +128 -0
- package/server/admin/index.html +462 -1
- package/server/admin/login.html +125 -38
- package/server/admin/sql.js +134 -0
- package/server/admin/users.js +147 -0
- package/server/db/database.js +19 -0
- package/server/middleware/adminAuth.js +37 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +4 -4
- package/server/routes/admin.js +301 -4
- package/server/services/account/admin_two_factor.js +132 -0
package/server/admin/index.html
CHANGED
|
@@ -309,6 +309,367 @@
|
|
|
309
309
|
color: var(--text-muted) !important;
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
+
/* ── Stats grid ────────────────────────────────────────────── */
|
|
313
|
+
.stat-grid {
|
|
314
|
+
display: grid;
|
|
315
|
+
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
316
|
+
gap: 10px;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.stat-card {
|
|
320
|
+
background: var(--bg-secondary);
|
|
321
|
+
border: 1px solid var(--border);
|
|
322
|
+
border-radius: 8px;
|
|
323
|
+
padding: 18px 16px 14px;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.stat-value {
|
|
327
|
+
font-size: 28px;
|
|
328
|
+
font-weight: 700;
|
|
329
|
+
letter-spacing: -0.5px;
|
|
330
|
+
color: var(--text);
|
|
331
|
+
line-height: 1;
|
|
332
|
+
font-variant-numeric: tabular-nums;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.stat-label {
|
|
336
|
+
font-size: 11px;
|
|
337
|
+
font-weight: 600;
|
|
338
|
+
text-transform: uppercase;
|
|
339
|
+
letter-spacing: 0.06em;
|
|
340
|
+
color: var(--text-muted);
|
|
341
|
+
margin-top: 8px;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/* ── Analytics tables ──────────────────────────────────────── */
|
|
345
|
+
.analytics-table {
|
|
346
|
+
width: 100%;
|
|
347
|
+
border-collapse: collapse;
|
|
348
|
+
font-size: 13px;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.analytics-table th {
|
|
352
|
+
text-align: left;
|
|
353
|
+
font-size: 10px;
|
|
354
|
+
font-weight: 600;
|
|
355
|
+
text-transform: uppercase;
|
|
356
|
+
letter-spacing: 0.07em;
|
|
357
|
+
color: var(--text-muted);
|
|
358
|
+
padding: 6px 8px;
|
|
359
|
+
border-bottom: 1px solid var(--border);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.analytics-table td {
|
|
363
|
+
padding: 9px 8px;
|
|
364
|
+
border-bottom: 1px solid rgba(224,240,224,0.04);
|
|
365
|
+
color: var(--text-secondary);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.analytics-table tr:last-child td { border-bottom: none; }
|
|
369
|
+
.analytics-table tr:hover td { background: rgba(224,240,224,0.025); }
|
|
370
|
+
|
|
371
|
+
.cell-truncate {
|
|
372
|
+
max-width: 280px;
|
|
373
|
+
overflow: hidden;
|
|
374
|
+
text-overflow: ellipsis;
|
|
375
|
+
white-space: nowrap;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.run-badge {
|
|
379
|
+
display: inline-block;
|
|
380
|
+
padding: 2px 8px;
|
|
381
|
+
border-radius: 999px;
|
|
382
|
+
font-size: 10px;
|
|
383
|
+
font-weight: 600;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.run-completed { background: rgba(116,192,124,0.15); color: var(--success); }
|
|
387
|
+
.run-failed { background: rgba(222,138,120,0.15); color: var(--danger); }
|
|
388
|
+
.run-running { background: var(--accent-muted); color: var(--accent); }
|
|
389
|
+
.run-pending { background: rgba(126,136,119,0.15); color: var(--text-muted); }
|
|
390
|
+
|
|
391
|
+
/* ── User avatar ────────────────────────────────────────────── */
|
|
392
|
+
.user-avatar {
|
|
393
|
+
display: inline-flex;
|
|
394
|
+
align-items: center;
|
|
395
|
+
justify-content: center;
|
|
396
|
+
width: 28px;
|
|
397
|
+
height: 28px;
|
|
398
|
+
min-width: 28px;
|
|
399
|
+
border-radius: 50%;
|
|
400
|
+
background: var(--accent-muted);
|
|
401
|
+
color: var(--accent);
|
|
402
|
+
font-size: 12px;
|
|
403
|
+
font-weight: 700;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/* ── Mini bar ───────────────────────────────────────────────── */
|
|
407
|
+
.mini-bar {
|
|
408
|
+
height: 4px;
|
|
409
|
+
background: var(--bg-secondary);
|
|
410
|
+
border-radius: 999px;
|
|
411
|
+
width: 80px;
|
|
412
|
+
overflow: hidden;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.mini-bar-fill {
|
|
416
|
+
height: 100%;
|
|
417
|
+
background: var(--accent);
|
|
418
|
+
border-radius: 999px;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/* ── Users table ────────────────────────────────────────────── */
|
|
422
|
+
.users-table {
|
|
423
|
+
width: 100%;
|
|
424
|
+
border-collapse: collapse;
|
|
425
|
+
font-size: 13px;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.users-table th {
|
|
429
|
+
text-align: left;
|
|
430
|
+
font-size: 10px;
|
|
431
|
+
font-weight: 600;
|
|
432
|
+
text-transform: uppercase;
|
|
433
|
+
letter-spacing: 0.07em;
|
|
434
|
+
color: var(--text-muted);
|
|
435
|
+
padding: 6px 8px;
|
|
436
|
+
border-bottom: 1px solid var(--border);
|
|
437
|
+
white-space: nowrap;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.users-table td {
|
|
441
|
+
padding: 10px 8px;
|
|
442
|
+
border-bottom: 1px solid rgba(224,240,224,0.04);
|
|
443
|
+
vertical-align: middle;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.users-table tr:last-child td { border-bottom: none; }
|
|
447
|
+
.users-table tr:hover td { background: rgba(224,240,224,0.025); }
|
|
448
|
+
|
|
449
|
+
.gdpr-note {
|
|
450
|
+
font-size: 11px;
|
|
451
|
+
color: var(--text-muted);
|
|
452
|
+
margin-top: 14px;
|
|
453
|
+
padding: 10px 14px;
|
|
454
|
+
background: rgba(222, 138, 120, 0.05);
|
|
455
|
+
border: 1px solid rgba(222, 138, 120, 0.15);
|
|
456
|
+
border-radius: var(--radius-sm);
|
|
457
|
+
line-height: 1.5;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/* ── SQL editor ─────────────────────────────────────────────── */
|
|
461
|
+
.sql-toolbar {
|
|
462
|
+
display: flex;
|
|
463
|
+
align-items: center;
|
|
464
|
+
gap: 8px;
|
|
465
|
+
margin-bottom: 10px;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.sql-textarea {
|
|
469
|
+
width: 100%;
|
|
470
|
+
min-height: 140px;
|
|
471
|
+
background: var(--bg-input);
|
|
472
|
+
border: 1px solid var(--border-light);
|
|
473
|
+
border-radius: var(--radius-sm);
|
|
474
|
+
padding: 12px 14px;
|
|
475
|
+
color: var(--text);
|
|
476
|
+
font-family: var(--font-mono);
|
|
477
|
+
font-size: 13px;
|
|
478
|
+
line-height: 1.6;
|
|
479
|
+
resize: vertical;
|
|
480
|
+
outline: none;
|
|
481
|
+
tab-size: 2;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
.sql-textarea:focus {
|
|
485
|
+
border-color: var(--accent);
|
|
486
|
+
box-shadow: 0 0 0 3px var(--accent-muted);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.sql-meta {
|
|
490
|
+
display: flex;
|
|
491
|
+
align-items: center;
|
|
492
|
+
gap: 8px;
|
|
493
|
+
font-size: 12px;
|
|
494
|
+
color: var(--text-muted);
|
|
495
|
+
margin: 12px 0 8px;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
.sql-result-wrap {
|
|
499
|
+
overflow-x: auto;
|
|
500
|
+
border: 1px solid var(--border);
|
|
501
|
+
border-radius: var(--radius-sm);
|
|
502
|
+
max-height: 420px;
|
|
503
|
+
overflow-y: auto;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
.sql-result-table {
|
|
507
|
+
width: 100%;
|
|
508
|
+
border-collapse: collapse;
|
|
509
|
+
font-size: 12px;
|
|
510
|
+
font-family: var(--font-mono);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.sql-result-table th {
|
|
514
|
+
background: var(--bg-secondary);
|
|
515
|
+
padding: 7px 12px;
|
|
516
|
+
text-align: left;
|
|
517
|
+
font-size: 10px;
|
|
518
|
+
font-weight: 700;
|
|
519
|
+
text-transform: uppercase;
|
|
520
|
+
letter-spacing: 0.05em;
|
|
521
|
+
color: var(--text-muted);
|
|
522
|
+
white-space: nowrap;
|
|
523
|
+
position: sticky;
|
|
524
|
+
top: 0;
|
|
525
|
+
z-index: 1;
|
|
526
|
+
border-bottom: 1px solid var(--border-light);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
.sql-result-table td {
|
|
530
|
+
padding: 6px 12px;
|
|
531
|
+
border-bottom: 1px solid rgba(224,240,224,0.04);
|
|
532
|
+
color: var(--text-secondary);
|
|
533
|
+
white-space: nowrap;
|
|
534
|
+
max-width: 320px;
|
|
535
|
+
overflow: hidden;
|
|
536
|
+
text-overflow: ellipsis;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
.sql-result-table tr:last-child td { border-bottom: none; }
|
|
540
|
+
.sql-result-table tr:hover td { background: rgba(224,240,224,0.03); }
|
|
541
|
+
|
|
542
|
+
.sql-error {
|
|
543
|
+
padding: 12px 14px;
|
|
544
|
+
margin-top: 10px;
|
|
545
|
+
background: rgba(222,138,120,0.08);
|
|
546
|
+
border: 1px solid rgba(222,138,120,0.2);
|
|
547
|
+
border-radius: var(--radius-sm);
|
|
548
|
+
color: var(--danger);
|
|
549
|
+
font-family: var(--font-mono);
|
|
550
|
+
font-size: 12px;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/* ── Access / toggle ───────────────────────────────────────── */
|
|
554
|
+
.access-row {
|
|
555
|
+
display: flex;
|
|
556
|
+
align-items: flex-start;
|
|
557
|
+
justify-content: space-between;
|
|
558
|
+
gap: 20px;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
.access-label {
|
|
562
|
+
font-size: 14px;
|
|
563
|
+
font-weight: 600;
|
|
564
|
+
color: var(--text);
|
|
565
|
+
margin-bottom: 4px;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
.access-desc {
|
|
569
|
+
font-size: 12px;
|
|
570
|
+
color: var(--text-muted);
|
|
571
|
+
line-height: 1.5;
|
|
572
|
+
max-width: 480px;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
.access-desc code {
|
|
576
|
+
font-family: var(--font-mono);
|
|
577
|
+
font-size: 11px;
|
|
578
|
+
background: var(--bg-secondary);
|
|
579
|
+
padding: 1px 5px;
|
|
580
|
+
border-radius: 4px;
|
|
581
|
+
color: var(--accent);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
.access-hint-row {
|
|
585
|
+
display: flex;
|
|
586
|
+
align-items: center;
|
|
587
|
+
gap: 12px;
|
|
588
|
+
margin-top: 12px;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
.access-actions {
|
|
592
|
+
display: flex;
|
|
593
|
+
gap: 8px;
|
|
594
|
+
margin-top: 16px;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
.access-status {
|
|
598
|
+
font-size: 12px;
|
|
599
|
+
margin-top: 8px;
|
|
600
|
+
min-height: 16px;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/* Toggle switch */
|
|
604
|
+
.toggle {
|
|
605
|
+
position: relative;
|
|
606
|
+
width: 40px;
|
|
607
|
+
height: 22px;
|
|
608
|
+
flex-shrink: 0;
|
|
609
|
+
cursor: pointer;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
.toggle input { position: absolute; opacity: 0; width: 0; height: 0; }
|
|
613
|
+
|
|
614
|
+
.toggle-track {
|
|
615
|
+
position: absolute;
|
|
616
|
+
inset: 0;
|
|
617
|
+
background: var(--bg-secondary);
|
|
618
|
+
border: 1px solid var(--border-light);
|
|
619
|
+
border-radius: 999px;
|
|
620
|
+
transition: background 0.18s, border-color 0.18s;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
.toggle input:checked ~ .toggle-track {
|
|
624
|
+
background: var(--accent);
|
|
625
|
+
border-color: var(--accent);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
.toggle-thumb {
|
|
629
|
+
position: absolute;
|
|
630
|
+
top: 3px;
|
|
631
|
+
left: 3px;
|
|
632
|
+
width: 16px;
|
|
633
|
+
height: 16px;
|
|
634
|
+
background: #fff;
|
|
635
|
+
border-radius: 50%;
|
|
636
|
+
transition: transform 0.18s;
|
|
637
|
+
pointer-events: none;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.toggle input:checked ~ .toggle-thumb { transform: translateX(18px); }
|
|
641
|
+
|
|
642
|
+
/* API key display */
|
|
643
|
+
.api-key-hint {
|
|
644
|
+
font-family: var(--font-mono);
|
|
645
|
+
font-size: 13px;
|
|
646
|
+
color: var(--text-secondary);
|
|
647
|
+
background: var(--bg-secondary);
|
|
648
|
+
padding: 4px 10px;
|
|
649
|
+
border-radius: 6px;
|
|
650
|
+
letter-spacing: 0.04em;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
.api-key-full {
|
|
654
|
+
font-family: var(--font-mono);
|
|
655
|
+
font-size: 12px;
|
|
656
|
+
color: var(--text);
|
|
657
|
+
background: var(--bg-secondary);
|
|
658
|
+
padding: 6px 10px;
|
|
659
|
+
border-radius: 6px;
|
|
660
|
+
word-break: break-all;
|
|
661
|
+
flex: 1;
|
|
662
|
+
user-select: all;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
.key-reveal-box {
|
|
666
|
+
background: rgba(225, 176, 82, 0.07);
|
|
667
|
+
border: 1px solid rgba(225, 176, 82, 0.25);
|
|
668
|
+
border-radius: var(--radius-sm);
|
|
669
|
+
padding: 12px 14px;
|
|
670
|
+
margin-top: 14px;
|
|
671
|
+
}
|
|
672
|
+
|
|
312
673
|
/* ── Provider rows ─────────────────────────────────────────── */
|
|
313
674
|
.provider-row {
|
|
314
675
|
display: grid;
|
|
@@ -379,7 +740,7 @@
|
|
|
379
740
|
</div>
|
|
380
741
|
|
|
381
742
|
<nav aria-label="Admin navigation">
|
|
382
|
-
<div class="nav-section-label">
|
|
743
|
+
<div class="nav-section-label">Monitor</div>
|
|
383
744
|
|
|
384
745
|
<button class="nav-item active" data-page="overview" onclick="showPage('overview',this)">
|
|
385
746
|
<svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
@@ -402,6 +763,38 @@
|
|
|
402
763
|
Updates
|
|
403
764
|
</button>
|
|
404
765
|
|
|
766
|
+
<div class="nav-section-label" style="margin-top:8px;">Manage</div>
|
|
767
|
+
|
|
768
|
+
<button class="nav-item" data-page="analytics" onclick="showPage('analytics',this)">
|
|
769
|
+
<svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
770
|
+
<path d="M2 11a1 1 0 011-1h2a1 1 0 011 1v5a1 1 0 01-1 1H3a1 1 0 01-1-1v-5zM8 7a1 1 0 011-1h2a1 1 0 011 1v9a1 1 0 01-1 1H9a1 1 0 01-1-1V7zM14 4a1 1 0 011-1h2a1 1 0 011 1v12a1 1 0 01-1 1h-2a1 1 0 01-1-1V4z"/>
|
|
771
|
+
</svg>
|
|
772
|
+
Analytics
|
|
773
|
+
</button>
|
|
774
|
+
|
|
775
|
+
<button class="nav-item" data-page="users" onclick="showPage('users',this)">
|
|
776
|
+
<svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
777
|
+
<path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z"/>
|
|
778
|
+
</svg>
|
|
779
|
+
Users
|
|
780
|
+
</button>
|
|
781
|
+
|
|
782
|
+
<button class="nav-item" data-page="sql" onclick="showPage('sql',this)">
|
|
783
|
+
<svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
784
|
+
<path fill-rule="evenodd" d="M3 5a2 2 0 012-2h10a2 2 0 012 2v10a2 2 0 01-2 2H5a2 2 0 01-2-2V5zm11 1H6v8l4-2 4 2V6z" clip-rule="evenodd"/>
|
|
785
|
+
</svg>
|
|
786
|
+
SQL Editor
|
|
787
|
+
</button>
|
|
788
|
+
|
|
789
|
+
<div class="nav-section-label" style="margin-top:8px;">Settings</div>
|
|
790
|
+
|
|
791
|
+
<button class="nav-item" data-page="access" onclick="showPage('access',this)">
|
|
792
|
+
<svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
793
|
+
<path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/>
|
|
794
|
+
</svg>
|
|
795
|
+
Access
|
|
796
|
+
</button>
|
|
797
|
+
|
|
405
798
|
<button class="nav-item" data-page="providers" onclick="showPage('providers',this)">
|
|
406
799
|
<svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
407
800
|
<path fill-rule="evenodd" d="M18 8a6 6 0 01-7.743 5.743L10 14l-1 1-1 1H6v2H2v-4l4.257-4.257A6 6 0 1118 8zm-6-4a1 1 0 100 2 2 2 0 012 2 1 1 0 102 0 4 4 0 00-4-4z" clip-rule="evenodd"/>
|
|
@@ -493,6 +886,70 @@
|
|
|
493
886
|
</div>
|
|
494
887
|
</section>
|
|
495
888
|
|
|
889
|
+
<!-- Analytics -->
|
|
890
|
+
<section id="page-analytics" class="page" aria-label="Analytics">
|
|
891
|
+
<div class="page-header">
|
|
892
|
+
<div>
|
|
893
|
+
<h1 class="page-title">Analytics</h1>
|
|
894
|
+
<p class="page-subtitle">Usage statistics, top users, and recent activity</p>
|
|
895
|
+
</div>
|
|
896
|
+
<span class="refresh-hint" id="analytics-ts" aria-live="polite"></span>
|
|
897
|
+
</div>
|
|
898
|
+
<div class="content">
|
|
899
|
+
<div id="analytics-content"><div class="empty"><span class="spinner"></span></div></div>
|
|
900
|
+
</div>
|
|
901
|
+
</section>
|
|
902
|
+
|
|
903
|
+
<!-- Users -->
|
|
904
|
+
<section id="page-users" class="page" aria-label="User Management">
|
|
905
|
+
<div class="page-header">
|
|
906
|
+
<div>
|
|
907
|
+
<h1 class="page-title">Users</h1>
|
|
908
|
+
<p class="page-subtitle">Account management — force logout, GDPR data erasure</p>
|
|
909
|
+
</div>
|
|
910
|
+
<span class="toolbar-count" id="users-count" aria-live="polite" style="padding-top:6px;"></span>
|
|
911
|
+
</div>
|
|
912
|
+
<div class="content">
|
|
913
|
+
<div class="card">
|
|
914
|
+
<div class="toolbar" style="margin-bottom:14px;">
|
|
915
|
+
<input type="search" id="user-search" placeholder="Search by username or email…"
|
|
916
|
+
oninput="onUserSearch()" style="width:280px;padding:7px 12px;font-size:13px;" aria-label="Search users">
|
|
917
|
+
<span class="spacer"></span>
|
|
918
|
+
<button class="btn btn-ghost" onclick="loadUsers()">Refresh</button>
|
|
919
|
+
</div>
|
|
920
|
+
<div id="users-table-wrap"><div class="empty">Loading…</div></div>
|
|
921
|
+
</div>
|
|
922
|
+
</div>
|
|
923
|
+
</section>
|
|
924
|
+
|
|
925
|
+
<!-- SQL Editor -->
|
|
926
|
+
<section id="page-sql" class="page" aria-label="SQL Editor">
|
|
927
|
+
<div class="page-header">
|
|
928
|
+
<div>
|
|
929
|
+
<h1 class="page-title">SQL Editor</h1>
|
|
930
|
+
<p class="page-subtitle">Read-only SELECT queries against the live database — results capped at 500 rows</p>
|
|
931
|
+
</div>
|
|
932
|
+
</div>
|
|
933
|
+
<div class="content">
|
|
934
|
+
<div class="card">
|
|
935
|
+
<div id="sql-content"><div class="empty"><span class="spinner"></span></div></div>
|
|
936
|
+
</div>
|
|
937
|
+
</div>
|
|
938
|
+
</section>
|
|
939
|
+
|
|
940
|
+
<!-- Access -->
|
|
941
|
+
<section id="page-access" class="page" aria-label="Access Settings">
|
|
942
|
+
<div class="page-header">
|
|
943
|
+
<div>
|
|
944
|
+
<h1 class="page-title">Access</h1>
|
|
945
|
+
<p class="page-subtitle">Signup policy and programmatic API key management</p>
|
|
946
|
+
</div>
|
|
947
|
+
</div>
|
|
948
|
+
<div class="content">
|
|
949
|
+
<div id="access-content"><div class="empty"><span class="spinner"></span></div></div>
|
|
950
|
+
</div>
|
|
951
|
+
</section>
|
|
952
|
+
|
|
496
953
|
<!-- Providers -->
|
|
497
954
|
<section id="page-providers" class="page" aria-label="Providers">
|
|
498
955
|
<div class="page-header">
|
|
@@ -528,5 +985,9 @@
|
|
|
528
985
|
</main>
|
|
529
986
|
|
|
530
987
|
<script src="/admin/admin.js"></script>
|
|
988
|
+
<script src="/admin/analytics.js"></script>
|
|
989
|
+
<script src="/admin/users.js"></script>
|
|
990
|
+
<script src="/admin/sql.js"></script>
|
|
991
|
+
<script src="/admin/access.js"></script>
|
|
531
992
|
</body>
|
|
532
993
|
</html>
|
package/server/admin/login.html
CHANGED
|
@@ -85,47 +85,95 @@
|
|
|
85
85
|
</div>
|
|
86
86
|
</div>
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
<
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
<
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
88
|
+
<!-- Step 1: credentials -->
|
|
89
|
+
<div id="step-credentials">
|
|
90
|
+
<h1 class="login-heading">Sign in</h1>
|
|
91
|
+
<p class="login-sub">
|
|
92
|
+
Credentials are in your <code>.env</code> or via <code>neoagent admin</code>
|
|
93
|
+
</p>
|
|
94
|
+
|
|
95
|
+
<div class="alert alert-error" id="error" role="alert"></div>
|
|
96
|
+
|
|
97
|
+
<form id="form" novalidate>
|
|
98
|
+
<div class="field">
|
|
99
|
+
<label for="username">Username</label>
|
|
100
|
+
<input
|
|
101
|
+
id="username"
|
|
102
|
+
type="text"
|
|
103
|
+
autocomplete="username"
|
|
104
|
+
autofocus
|
|
105
|
+
required
|
|
106
|
+
spellcheck="false"
|
|
107
|
+
autocorrect="off"
|
|
108
|
+
autocapitalize="none"
|
|
109
|
+
>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="field">
|
|
112
|
+
<label for="password">Password</label>
|
|
113
|
+
<input
|
|
114
|
+
id="password"
|
|
115
|
+
type="password"
|
|
116
|
+
autocomplete="current-password"
|
|
117
|
+
required
|
|
118
|
+
>
|
|
119
|
+
</div>
|
|
120
|
+
<button type="submit" class="btn btn-primary submit-btn" id="submit-btn">
|
|
121
|
+
Sign in
|
|
122
|
+
</button>
|
|
123
|
+
</form>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<!-- Step 2: 2FA code (shown after password is accepted) -->
|
|
127
|
+
<div id="step-totp" style="display:none;">
|
|
128
|
+
<h1 class="login-heading">Two-factor authentication</h1>
|
|
129
|
+
<p class="login-sub">Enter the 6-digit code from your authenticator app, or a recovery code.</p>
|
|
130
|
+
|
|
131
|
+
<div class="alert alert-error" id="totp-error" role="alert"></div>
|
|
132
|
+
|
|
133
|
+
<form id="totp-form" novalidate>
|
|
134
|
+
<div class="field">
|
|
135
|
+
<label for="totp-code">Authenticator code</label>
|
|
136
|
+
<input
|
|
137
|
+
id="totp-code"
|
|
138
|
+
type="text"
|
|
139
|
+
inputmode="numeric"
|
|
140
|
+
autocomplete="one-time-code"
|
|
141
|
+
maxlength="11"
|
|
142
|
+
placeholder="123 456"
|
|
143
|
+
spellcheck="false"
|
|
144
|
+
autocorrect="off"
|
|
145
|
+
autocapitalize="none"
|
|
146
|
+
required
|
|
147
|
+
>
|
|
148
|
+
</div>
|
|
149
|
+
<button type="submit" class="btn btn-primary submit-btn" id="totp-btn">
|
|
150
|
+
Verify
|
|
151
|
+
</button>
|
|
152
|
+
</form>
|
|
153
|
+
<div style="margin-top:14px;text-align:center;">
|
|
154
|
+
<button type="button" class="btn btn-ghost" style="font-size:12px;padding:4px 10px;"
|
|
155
|
+
onclick="backToCredentials()">← Back</button>
|
|
117
156
|
</div>
|
|
118
|
-
|
|
119
|
-
Sign in
|
|
120
|
-
</button>
|
|
121
|
-
</form>
|
|
157
|
+
</div>
|
|
122
158
|
</div>
|
|
123
159
|
</div>
|
|
124
160
|
|
|
125
161
|
<script>
|
|
126
|
-
const form
|
|
162
|
+
const form = document.getElementById('form');
|
|
127
163
|
const submitBtn = document.getElementById('submit-btn');
|
|
128
|
-
const errorEl
|
|
164
|
+
const errorEl = document.getElementById('error');
|
|
165
|
+
const totpForm = document.getElementById('totp-form');
|
|
166
|
+
const totpBtn = document.getElementById('totp-btn');
|
|
167
|
+
const totpError = document.getElementById('totp-error');
|
|
168
|
+
|
|
169
|
+
function showStep(step) {
|
|
170
|
+
document.getElementById('step-credentials').style.display = step === 1 ? '' : 'none';
|
|
171
|
+
document.getElementById('step-totp').style.display = step === 2 ? '' : 'none';
|
|
172
|
+
if (step === 2) setTimeout(() => document.getElementById('totp-code')?.focus(), 50);
|
|
173
|
+
if (step === 1) setTimeout(() => document.getElementById('username')?.focus(), 50);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function backToCredentials() { showStep(1); }
|
|
129
177
|
|
|
130
178
|
form.addEventListener('submit', async (e) => {
|
|
131
179
|
e.preventDefault();
|
|
@@ -143,12 +191,18 @@
|
|
|
143
191
|
}),
|
|
144
192
|
});
|
|
145
193
|
|
|
146
|
-
|
|
147
|
-
|
|
194
|
+
const body = await res.json().catch(() => ({}));
|
|
195
|
+
|
|
196
|
+
if (res.ok || body.requiresTwoFactor) {
|
|
197
|
+
if (body.requiresTwoFactor) {
|
|
198
|
+
// Password accepted — go to 2FA step
|
|
199
|
+
showStep(2);
|
|
200
|
+
} else {
|
|
201
|
+
window.location.replace('/admin');
|
|
202
|
+
}
|
|
148
203
|
return;
|
|
149
204
|
}
|
|
150
205
|
|
|
151
|
-
const body = await res.json().catch(() => ({}));
|
|
152
206
|
errorEl.textContent = body.error || 'Invalid credentials';
|
|
153
207
|
errorEl.classList.add('visible');
|
|
154
208
|
} catch {
|
|
@@ -159,6 +213,39 @@
|
|
|
159
213
|
submitBtn.textContent = 'Sign in';
|
|
160
214
|
}
|
|
161
215
|
});
|
|
216
|
+
|
|
217
|
+
totpForm.addEventListener('submit', async (e) => {
|
|
218
|
+
e.preventDefault();
|
|
219
|
+
totpError.classList.remove('visible');
|
|
220
|
+
totpBtn.disabled = true;
|
|
221
|
+
totpBtn.textContent = 'Verifying…';
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
const code = document.getElementById('totp-code').value.trim();
|
|
225
|
+
const res = await fetch('/admin/api/2fa/verify', {
|
|
226
|
+
method: 'POST',
|
|
227
|
+
headers: { 'Content-Type': 'application/json' },
|
|
228
|
+
body: JSON.stringify({ code }),
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
if (res.ok) {
|
|
232
|
+
window.location.replace('/admin');
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const body = await res.json().catch(() => ({}));
|
|
237
|
+
totpError.textContent = body.error || 'Invalid code';
|
|
238
|
+
totpError.classList.add('visible');
|
|
239
|
+
document.getElementById('totp-code').value = '';
|
|
240
|
+
document.getElementById('totp-code').focus();
|
|
241
|
+
} catch {
|
|
242
|
+
totpError.textContent = 'Network error';
|
|
243
|
+
totpError.classList.add('visible');
|
|
244
|
+
} finally {
|
|
245
|
+
totpBtn.disabled = false;
|
|
246
|
+
totpBtn.textContent = 'Verify';
|
|
247
|
+
}
|
|
248
|
+
});
|
|
162
249
|
</script>
|
|
163
250
|
</body>
|
|
164
251
|
</html>
|